Opening the gnuplot failed: pipe: Too many open files
Bug category
- [ ] bug - compilation error
- [ ] bug - compilation warning
- [x ] bug - runtime error
- [ ] bug - runtime warning
- [ ] bug - logic error
Describe the bug Hi, I really like the library you created. I found a problem when creating a lot of plots. If a lot of plots are created silently and then saved, matplot seems not to close the file connections.
Steps to Reproduce
My minimal working example is:
#include
using namespace matplot;
for (int i = 0; i < 1000; i++) {
std::vector<double> a = {1.0,2.0,3.0,4.0};
std::vector<double> b = {1.0,2.0,3.0,4.0};
auto fig = figure(true);
fig->size(1200, 800);
auto ax = axes(fig);
ax -> plot(a,b);
save("test" + std::to_string(i) + ".svg");
}
}
Output
After around 251 plots (depends on system, I think), I get this console output for all following plots:
Opening the gnuplot failed: pipe: Too many open files Please install gnuplot 5.2.6+: http://www.gnuplot.info
Platform
- [ ] cross-platform issue - linux
- [ x] cross-platform issue - windows
- [x ] cross-platform issue - macos
Environment Details:
- OS: MacOS
- OS Version: Sonoma 14.4
- Compiler: g++-14 (cstd-20)
- Compiler version:
Additional context
Hello, would u share your solution please?
I used standard GNU Plot library at the end to control the piping myself. It was not that hard overall...
hey my friend @steppenhahni you can use this code sample. U only make 1 frame and keep updating it. I leave the class here. I'm pretty sure u can customize it for ur usage:
The Header File:
#ifndef SCALEHISTOGRAM_H
#define SCALEHISTOGRAM_H
#include <vector>
#include <string>
#include <mutex>
#include <thread>
#include <condition_variable>
#include <atomic>
#include <queue>
#include <deque>
#include <boost/lockfree/spsc_queue.hpp>
#include "matplot/matplot.h"
/* NEW: small POD to move both pieces through the lock-free queue */
struct ScaleSample {
double value;
long long ts; // caller-provided timestamp
};
class ScaleHistogram {
public:
// explicit ScaleHistogram(std::size_t numBins = 30, std::size_t maxFiles = 100);
explicit ScaleHistogram(
std::size_t bins = 50,
std::size_t maxFiles = 100,
std::size_t batch = 64,
std::chrono::milliseconds flush = std::chrono::milliseconds(200));
~ScaleHistogram();
/** Append a new scale and emit / overwrite a PNG. Thread-safe. */
// void plot(double scale);
/** wait-free push: supply scale + your own timestamp */
void plotAsync(double scale, long long myTimeStamp);
/** Stop async processing and cleanup */
// void stopAsync();
private:
void pruneOldPNGs(); // keep only maxFiles_ files
std::string timeStamp() const;
void asyncPlotWorker(); // Background thread worker
// ─── config ───────────────────────────────────────
const std::size_t bins_;
const std::size_t maxFiles_;
const std::size_t batchSize_;
const std::chrono::milliseconds flushInterval_;
// ─── data received from producers (SPSC, lock-free) ───
boost::lockfree::spsc_queue<ScaleSample, boost::lockfree::capacity<8192>> q_;
// ─── worker thread ────────────────────────────────
std::thread worker_;
std::atomic<bool> stop_{false};
// ─── persistent Matplot++ artefacts ───────────────
std::vector<double> all_; // full history
matplot::figure_handle fig_; // created once
matplot::histogram_handle hist_;// handle to bars
matplot::axes_handle ax_; // ← NEW
// bool firstDraw_{true}; // ← NEW
// ─── housekeeping ────────────────────────────────
std::deque<std::string> written_;
};
#endif //SCALEHISTOGRAM_H
.cc FIle:
#include "ScaleHistogram.h"
#include <filesystem>
#include <chrono>
#include <iomanip>
#include <sstream>
namespace plt = matplot;
namespace fs = std::filesystem;
ScaleHistogram::ScaleHistogram(std::size_t bins,
std::size_t maxFiles,
std::size_t batch,
std::chrono::milliseconds flush)
: bins_(bins),
maxFiles_(maxFiles),
batchSize_(batch),
flushInterval_(flush),
q_()
{
fs::create_directories("plots");
fig_ = plt::figure(true); // head-less
ax_ = fig_->current_axes();
ax_->title("Histogram of Scales");
ax_->xlabel("scale");
ax_->ylabel("count");
ax_->hist(std::vector<double>{}, bins_); // empty bars
// hist_ = plt::hist(std::vector<double>{}, bins_); // empty bars
// plt::title("Histogram of Scales");
// plt::xlabel("scale");
// plt::ylabel("count");
worker_ = std::thread(&ScaleHistogram::asyncPlotWorker, this);
}
ScaleHistogram::~ScaleHistogram()
{
stop_.store(true, std::memory_order_relaxed);
if (worker_.joinable()) worker_.join();
}
/* ---------- PUBLIC: wait-free push ---------- */
void ScaleHistogram::plotAsync(double scale, long long myTimeStamp)
{
/* drops sample if queue momentarily full */
q_.push(ScaleSample{scale, myTimeStamp});
}
/* ---------- WORKER ---------- */
void ScaleHistogram::asyncPlotWorker()
{
std::vector<double> local;
local.reserve(batchSize_);
long long lastTS = 0; // remember latest ts
auto nextFlush = std::chrono::steady_clock::now() + flushInterval_;
while (!stop_.load(std::memory_order_relaxed)) {
ScaleSample s;
while (q_.pop(s)) {
local.push_back(s.value);
lastTS = s.ts; // keep the most recent
if (local.size() >= batchSize_)
break;
}
const auto now = std::chrono::steady_clock::now();
if (local.empty() && now < nextFlush) {
std::this_thread::sleep_until(nextFlush);
continue;
}
/* move batch into history */
all_.insert(all_.end(),
std::make_move_iterator(local.begin()),
std::make_move_iterator(local.end()));
local.clear();
if (all_.empty()) { // ← NEW: nothing to draw yet
nextFlush = std::chrono::steady_clock::now() + flushInterval_;
continue;
}
// redraw just the bars
ax_->clear(); // wipes only series; title/labels stay
ax_->hist(all_, bins_);
const std::string file =
"plots/ScaleHistogram_" + std::to_string(lastTS) + ".png";
plt::save(fig_, file);
written_.push_back(file);
pruneOldPNGs();
nextFlush = std::chrono::steady_clock::now() + flushInterval_;
}
}
/* ---------- helpers unchanged except for includes ---------- */
void ScaleHistogram::pruneOldPNGs()
{
while (written_.size() > maxFiles_) {
std::error_code es;
fs::remove(written_.front(), es);
written_.pop_front();
}
}
std::string ScaleHistogram::timeStamp() const
{
using clock = std::chrono::system_clock;
auto now = clock::now();
auto tt = clock::to_time_t(now);
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
now.time_since_epoch()) % 1000;
std::ostringstream ss;
ss << std::put_time(std::localtime(&tt), "%Y%m%d_%H%M%S")
<< '_' << std::setw(3) << std::setfill('0') << ms.count();
return ss.str();
}