Doxygen
Loading...
Searching...
No Matches
DotRunner Class Reference

Helper class to run dot from doxygen. More...

#include <src/dotrunner.h>

Collaboration diagram for DotRunner:

Public Member Functions

 DotRunner ()
bool run (const DotJobs &jobs)
 Runs dot for all given jobs.

Static Public Member Functions

static bool readBoundingBox (const DString &fileName, int *width, int *height, bool isEps)

Private Attributes

DString m_dotExe

Detailed Description

Helper class to run dot from doxygen.

Holds a queue of jobs, each specifying an input .dot file and output format. Call run() to execute all queued jobs, batched as a single dot invocation per output format using the -O flag for automatic output file naming.

Definition at line 29 of file dotrunner.h.

Constructor & Destructor Documentation

◆ DotRunner()

DotRunner::DotRunner ( )

Definition at line 277 of file dotrunner.cpp.

279{
280}
DString m_dotExe
Definition dotrunner.h:42
static DString verifiedDotPath
Definition doxygen.h:130

References m_dotExe.

Member Function Documentation

◆ readBoundingBox()

bool DotRunner::readBoundingBox ( const DString & fileName,
int * width,
int * height,
bool isEps )
static

Definition at line 150 of file dotrunner.cpp.

151{
152 std::ifstream f = Portable::openInputStream(fileName);
153 if (!f.is_open())
154 {
155 err("Failed to open file {} for extracting bounding box\n",fileName);
156 return false;
157 }
158
159 // read file contents into string 'contents'
160 std::stringstream buffer;
161 buffer << f.rdbuf();
162 std::string contents = buffer.str();
163
164 // start of bounding box marker we are looking for
165 const std::string boundingBox = isEps ? "%%PageBoundingBox:" : "/MediaBox [";
166
167 // helper routine to extract the bounding boxes width and height
168 auto extractBoundingBox = [&fileName,&boundingBox,&width,&height](const char *s) -> bool
169 {
170 int x=0, y=0;
171 double w=0, h=0;
172 if (sscanf(s+boundingBox.length(),"%d %d %lf %lf",&x,&y,&w,&h)==4)
173 {
174 *width = static_cast<int>(std::ceil(w));
175 *height = static_cast<int>(std::ceil(h));
176 return true;
177 }
178 err("Failed to extract bounding box from generated diagram file {}\n",fileName);
179 return false;
180 };
181
182 // compressed segment start and end markers
183 const std::string streamStart = "stream\n";
184 const std::string streamEnd = "\nendstream";
185
186 auto detectDeflateStreamStart = [&streamStart](const char *s)
187 {
188 size_t len = streamStart.length();
189 bool streamOK = strncmp(s,streamStart.c_str(),len)==0;
190 if (streamOK) // ASCII marker matches, check stream header bytes as well
191 {
192 unsigned short header1 = static_cast<unsigned char>(s[len])<<8; // CMF byte
193 if (header1) // not end of string
194 {
195 unsigned short header = (static_cast<unsigned char>(s[len+1])) | header1; // FLG byte
196 // check for correct header (see https://www.rfc-editor.org/rfc/rfc1950)
197 return ((header&0x8F20)==0x0800) && (header%31)==0;
198 }
199 }
200 return false;
201 };
202
203 const size_t l = contents.length();
204 size_t i=0;
205 while (i<l)
206 {
207 if (!isEps && contents[i]=='s' && detectDeflateStreamStart(&contents[i]))
208 { // compressed stream start
209 int col=17;
210 i+=streamStart.length();
211 const size_t start=i;
212 DBG(("---- start stream at offset %08x\n",(int)i));
213 while (i<l)
214 {
215 if (contents[i]=='\n' && strncmp(&contents[i],streamEnd.c_str(),streamEnd.length())==0)
216 { // compressed block found in range [start..i]
217 DBG(("\n---- end stream at offset %08x\n",(int)i));
218 // decompress it into decompressBuf
219 std::vector<char> decompressBuf;
220 const char *source = &contents[start];
221 const size_t sourceLen = i-start;
222 size_t sourcePos = 0;
223 decompressBuf.reserve(sourceLen*2);
224 auto getter = [source,&sourcePos,sourceLen]() -> int {
225 return sourcePos<sourceLen ? static_cast<unsigned char>(source[sourcePos++]) : EOF;
226 };
227 auto putter = [&decompressBuf](const char c) -> int {
228 decompressBuf.push_back(c); return c;
229 };
230 Deflate(getter,putter);
231 // convert decompression buffer to string
232 std::string s(decompressBuf.begin(), decompressBuf.end());
233 DBG(("decompressed_data=[[[\n%s\n]]]\n",s.c_str()));
234 // search for bounding box marker
235 const size_t idx = s.find(boundingBox);
236 if (idx!=std::string::npos) // found bounding box in uncompressed data
237 {
238 return extractBoundingBox(s.c_str()+idx);
239 }
240 // continue searching after end stream marker
241 i+=streamEnd.length();
242 break;
243 }
244 else // compressed stream character
245 {
246 if (col>16) { col=0; DBG(("\n%08x: ",static_cast<int>(i))); }
247 DBG(("%02x ",static_cast<unsigned char>(contents[i])));
248 col++;
249 i++;
250 }
251 }
252 }
253 else if (((isEps && contents[i]=='%') || (!isEps && contents[i]=='/')) &&
254 strncmp(&contents[i],boundingBox.c_str(),boundingBox.length())==0)
255 { // uncompressed bounding box
256 return extractBoundingBox(&contents[i]);
257 }
258 else // uncompressed stream character
259 {
260 i++;
261 }
262 }
263 err("Failed to find bounding box in generated diagram file {}\n",fileName);
264 // nothing found
265 return false;
266}
#define DBG(x)
Definition dotrunner.cpp:74
#define err(fmt,...)
Definition message.h:127
std::ifstream openInputStream(const DString &name, bool binary=false, bool openAtEnd=false)
Definition portable.cpp:692

References DBG, err, and Portable::openInputStream().

Referenced by run(), and DotFilePatcher::writeVecGfxFigure().

◆ run()

bool DotRunner::run ( const DotJobs & jobs)

Runs dot for all given jobs.

For each unique format, a single dot invocation is made with -O and all input files for that format.

Definition at line 282 of file dotrunner.cpp.

283{
284 if (dotJobs.empty()) return true;
285
286 // Group jobs by format, then by directory so we can cd once per group
287 std::map<std::string, std::map<std::string, std::vector<const DotJob*>>> byFormatAndDir;
288 for (const auto &job : dotJobs)
289 {
290 byFormatAndDir[job.format.str()][job.absPath.str()].push_back(&job);
291 }
292
293 std::mt19937 rng(std::random_device{}());
294 bool ok = true;
295 size_t prev=0;
296 for (const auto &[fmtStr, byDir] : byFormatAndDir)
297 {
298 DString format = DString(fmtStr);
299 DString extension = getDotImageExtension(format);
300 DString extGen = getDotImageExtensionGenerated(format);
301
302 for (const auto &[dirStr, jobs] : byDir)
303 {
304 std::string oldDir = Dir::currentDirPath();
305 Dir::setCurrent(dirStr);
306
307 // settings controlling how to distribute the graphs over threads and batches
308 const size_t numThreads = static_cast<size_t>(Config_getInt(DOT_NUM_THREADS));
309 const size_t batchSize = static_cast<size_t>(Config_getInt(DOT_BATCH_SIZE));
310 const size_t exeLen = m_dotExe.length() + 1; // "exe " prefix
311 const size_t maxArgLen = 32000-exeLen; // Windows CreateProcess limit is 32767; keep safe margin
312
313 // create a pseudo random ordering in which to process the dot files
314 std::vector<size_t> indices(jobs.size());
315 std::iota(indices.begin(), indices.end(), 0);
316 std::shuffle(indices.begin(), indices.end(), rng);
317
318 // helper to keep track of dot command to run later
319 struct CommandArgument
320 {
321 CommandArgument(const DString &args) : arguments(args) {}
322 DString arguments;
323 size_t numDotFiles = 0;
324 const DotJob *firstJob = nullptr;
325 };
326
327 std::vector<CommandArgument> partialCommands;
328 std::vector<CommandArgument> finalCommands;
329
330 bool hasImageMap = std::any_of(jobs.begin(),jobs.end(),[](const auto &j) { return j->generateImageMap; });
331
332 // each dot command has a command arguments of the form: -Tformat -O basename1.dot basename2.dot ...
333 DString baseArgs = DString("-T") + format;
334 if (hasImageMap) // if any image needs a map we generate one for all images
335 {
336 baseArgs += " -Tcmapx";
337 }
338 baseArgs += " -O";
339
340 // prepare partial commands for each thread (command is later skipped if numDotFiles==0).
341 for (size_t i=0; i<numThreads; i++)
342 {
343 partialCommands.emplace_back(baseArgs);
344 }
345
346 // split the jobs into batches per thread iterating in pseudo random order to fill each batch with a random selection of graphs
347 size_t index=0;
348 for (size_t i : indices)
349 {
350 const auto &job = jobs[i];
351 DString fileArg = DString(" ") + job->relDotName;
352 auto &cmd = partialCommands[index];
353 if (cmd.numDotFiles<batchSize && cmd.arguments.length()+fileArg.length()<maxArgLen) // still room in this batch
354 {
355 cmd.arguments+=fileArg;
356 cmd.numDotFiles++;
357 }
358 else // this batch is full, move to finished commands and start a new one
359 {
360 finalCommands.push_back(cmd);
361 cmd.arguments=baseArgs+fileArg;
362 cmd.numDotFiles=1;
363 }
364 if (cmd.firstJob==nullptr) cmd.firstJob=job;
365 index = (index+1)%numThreads;
366 }
367
368 // append partial commands to the final commands
369 finalCommands.insert(finalCommands.end(),partialCommands.begin(),partialCommands.end());
370
371 // now run the finalCommands.
372 if (Config_getInt(DOT_NUM_THREADS)<=1) // no threads to work with
373 {
374 for (const auto &cmd : finalCommands)
375 {
376 if (cmd.numDotFiles>0) // check if there are graphs to generate first
377 {
378 if (cmd.numDotFiles>1) // batch mode
379 {
380 msg("Running dot for graphs {}-{}/{}\n",prev+1,prev+cmd.numDotFiles,dotJobs.size());
381 }
382 else // single graph mode
383 {
384 msg("Running dot for graph {}/{}\n",prev+1,dotJobs.size());
385 }
386 prev+=cmd.numDotFiles;
387 int exitCode;
388 if ((exitCode = Portable::system(m_dotExe, cmd.arguments, false)) != 0)
389 {
390 err_full(cmd.firstJob->srcFile, 1,
391 "Problems running dot: exit code={}, command='{}', dir='{}', arguments='{}'",
392 exitCode, m_dotExe, dirStr, cmd.arguments);
393 ok = false;
394 }
395 }
396 }
397 }
398 else // use multiple threads to run instances of dot in parallel
399 {
400 ThreadPool workers(numThreads);
401 std::vector< std::future<size_t> > results;
402 for (auto & cmd: finalCommands)
403 {
404 if (cmd.numDotFiles>0)
405 {
406 auto locDirStr = dirStr;
407 auto process = [this,cmd,locDirStr]() -> size_t
408 {
409 int exitCode;
410 if ((exitCode = Portable::system(m_dotExe, cmd.arguments, false)) != 0)
411 {
412 err_full(cmd.firstJob->srcFile, 1,
413 "Problems running dot: exit code={}, command='{}', dir='{}', arguments='{}'",
414 exitCode, m_dotExe, locDirStr, cmd.arguments);
415 }
416 return cmd.numDotFiles;
417 };
418 results.emplace_back(workers.queue(process));
419 }
420 }
421 for (auto &f : results)
422 {
423 size_t numDotFiles = f.get();
424 if (numDotFiles>1) // batch mode
425 {
426 msg("Finished running dot for graphs {}-{}/{}\n",prev+1,prev+numDotFiles,dotJobs.size());
427 }
428 else // single graph mode
429 {
430 msg("Finished running dot for graph {}/{}\n",prev+1,dotJobs.size());
431 }
432 prev+=numDotFiles;
433 }
434 }
435
436 // Post-process each output file. dot -O appends the format suffix to the
437 // full input filename, so the output is absPath + relDotName + "." + extension.
438 // Rename to remove the .dot infix, producing absPath + baseName + "." + extension.
439 for (const auto *job : jobs)
440 {
441 DString base = job->absPath + getBaseNameOfOutput(job->relDotName);
442 DString dotOutput = job->absPath + job->relDotName + "." + extGen;
443 DString output = base + "." + extension;
444 Dir d;
445 if (!d.rename(dotOutput.str(), output.str()))
446 {
447 err("Failed to rename {} to {}!\n", dotOutput, output);
448 ok = false;
449 continue;
450 }
451 if (job->generateImageMap)
452 {
453 DString dotMapOutput = job->absPath + job->relDotName + ".cmapx";
454 DString mapOutput = base + ".map";
455 if (!d.rename(dotMapOutput.str(), mapOutput.str()))
456 {
457 err("Failed to rename {} to {}!\n", dotMapOutput, mapOutput);
458 ok = false;
459 continue;
460 }
461 }
462
463 if (format.startsWith("pdf"))
464 {
465 int width=0, height=0;
466 if (!readBoundingBox(output, &width, &height, false))
467 {
468 ok = false;
469 continue;
470 }
471 if ((width > MAX_LATEX_GRAPH_SIZE) || (height > MAX_LATEX_GRAPH_SIZE))
472 {
473 if (!resetPDFSize(width, height, base))
474 {
475 ok = false;
476 continue;
477 }
478 // Re-run dot for just this one file
479 DString rerunArgs = DString("-T") + format + " -O \"" + job->relDotName + "\"";
480 int exitCode;
481 if ((exitCode = Portable::system(m_dotExe, rerunArgs, false)) != 0)
482 {
483 err_full(job->srcFile, 1,
484 "Problems running dot: exit code={}, command='{}', dir='{}', arguments='{}'",
485 exitCode, m_dotExe, dirStr, rerunArgs);
486 ok = false;
487 }
488 else
489 {
490 Dir d2;
491 if (!d2.rename(dotOutput.str(), output.str()))
492 {
493 err("Failed to rename {} to {}!\n", dotOutput, output);
494 ok = false;
495 }
496 }
497 }
498 }
499 else if (format.startsWith("png"))
500 {
501 checkPngResult(output);
502 }
503 }
504 Dir::setCurrent(oldDir);
505 }
506 }
507
508 // Write .md5 files and clean up .dot files (once per unique dotFile)
509 std::set<std::string> processed;
510 for (const auto &job : dotJobs)
511 {
512 if (!processed.insert((job.absPath + job.relDotName).str()).second) continue;
513
514 if (!job.md5Hash.empty())
515 {
516 DString md5Name = job.absPath + getBaseNameOfOutput(job.relDotName) + ".md5";
517 FILE *f = Portable::fopen(md5Name, "w");
518 if (f)
519 {
520 fwrite(job.md5Hash.data(), 1, 32, f);
521 fclose(f);
522 }
523 }
524
525 if (Config_getBool(DOT_CLEANUP))
526 {
527 Portable::unlink(job.absPath + job.relDotName);
528 }
529 }
530
531 return ok;
532}
const std::string & str() const
Definition dstring.h:645
bool startsWith(const char *s) const
Definition dstring.h:600
size_t length() const
Returns the length of the string, not counting the 0-terminator.
Definition dstring.h:151
static std::string currentDirPath()
Definition dir.cpp:348
bool rename(const std::string &orgName, const std::string &newName, bool acceptsAbsPath=true) const
Definition dir.cpp:327
static bool setCurrent(const std::string &path)
Definition dir.cpp:356
static bool readBoundingBox(const DString &fileName, int *width, int *height, bool isEps)
#define Config_getInt(name)
Definition config.h:34
#define Config_getBool(name)
Definition config.h:33
static void checkPngResult(const DString &imgName)
Definition dotrunner.cpp:80
#define MAX_LATEX_GRAPH_SIZE
Definition dotrunner.cpp:71
static DString getBaseNameOfOutput(const DString &output)
static bool resetPDFSize(const int width, const int height, const DString &base)
#define msg(fmt,...)
Definition message.h:94
#define err_full(file, line, fmt,...)
Definition message.h:132
int system(const DString &command, const DString &args, bool commandHasConsole=true)
Definition portable.cpp:121
void unlink(const DString &fileName)
Definition portable.cpp:560
FILE * fopen(const DString &fileName, const DString &mode)
Definition portable.cpp:365
int fclose(FILE *f)
Definition portable.cpp:385
DString getDotImageExtension()
Definition util.cpp:4965
DString getDotImageExtensionGenerated(const DString &format)
Definition util.cpp:4975

References checkPngResult(), Config_getBool, Config_getInt, Dir::currentDirPath(), err, err_full, Portable::fopen(), getBaseNameOfOutput(), getDotImageExtension(), getDotImageExtensionGenerated(), DString::length(), m_dotExe, MAX_LATEX_GRAPH_SIZE, msg, ThreadPool::queue(), readBoundingBox(), Dir::rename(), resetPDFSize(), Dir::setCurrent(), DString::startsWith(), DString::str(), Portable::system(), and Portable::unlink().

Referenced by DotManager::run().

Member Data Documentation

◆ m_dotExe

DString DotRunner::m_dotExe
private

Definition at line 42 of file dotrunner.h.

Referenced by DotRunner(), and run().


The documentation for this class was generated from the following files: