Skip to content

Commit

Permalink
Add renderFormats method
Browse files Browse the repository at this point in the history
  • Loading branch information
mdaines committed Sep 25, 2024
1 parent b3c127f commit 2837bae
Showing 14 changed files with 344 additions and 77 deletions.
9 changes: 9 additions & 0 deletions packages/viz/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -2,6 +2,15 @@

## Unreleased

* Add renderFormats() method.

This method accepts an array of formats to render. Using this method avoids redundant parsing and layout when rendering the same graph in multiple formats, similar to specifying multiple formats when using the Graphviz command-line.

The return value is similar to the render() method, but the "output" value is an object keyed by format.

const result = viz.renderFormats("digraph { a -> b [href=\"https://example.com\"] }", ["svg", "cmapx"], { engine: "neato" });
result.output // => { "svg": ..., "cmapx": ... }

## 3.9.0

* Update Graphviz to 12.1.1.
59 changes: 28 additions & 31 deletions packages/viz/src/module/viz.c
Original file line number Diff line number Diff line change
@@ -158,48 +158,45 @@ void viz_free_graph(Agraph_t *g) {
}

EMSCRIPTEN_KEEPALIVE
char *viz_render_graph(Agraph_t *graph, const char *format, const char *engine) {
GVC_t *context = NULL;
char *data = NULL;
unsigned int length = 0;
int layout_error = 0;
int render_error = 0;
GVC_t *viz_create_context() {
return gvContextPlugins(lt_preloaded_symbols, 0);
}

// Initialize context
EMSCRIPTEN_KEEPALIVE
void viz_free_context(GVC_t *context) {
gvFinalize(context);
gvFreeContext(context);
}

context = gvContextPlugins(lt_preloaded_symbols, 0);
EMSCRIPTEN_KEEPALIVE
int viz_layout(GVC_t *context, Agraph_t *graph, const char *engine) {
return gvLayout(context, graph, engine);
}

// Reset errors
EMSCRIPTEN_KEEPALIVE
void viz_free_layout(GVC_t *context, Agraph_t *graph) {
gvFreeLayout(context, graph);
}

EMSCRIPTEN_KEEPALIVE
void viz_reset_errors() {
agseterrf(viz_errorf);
agseterr(AGWARN);
agreseterrors();
}

// Layout

layout_error = gvLayout(context, graph, engine);

// Render (if layout was successful)

if (!layout_error) {
render_error = gvRenderData(context, graph, format, &data, &length);

if (render_error) {
gvFreeRenderData(data);
data = NULL;
}
}
EMSCRIPTEN_KEEPALIVE
char *viz_render(GVC_t *context, Agraph_t *graph, const char *format) {
char *data = NULL;
unsigned int length = 0;
int render_error = 0;

// Free the layout and context
render_error = gvRenderData(context, graph, format, &data, &length);

if (graph) {
gvFreeLayout(context, graph);
if (render_error) {
gvFreeRenderData(data);
data = NULL;
}

gvFinalize(context);
gvFreeContext(context);

// Return the result (if successful, the rendered graph; otherwise, null)

return data;
}
20 changes: 19 additions & 1 deletion packages/viz/src/viz.mjs
Original file line number Diff line number Diff line change
@@ -17,8 +17,26 @@ class Viz {
return this.wrapper.getPluginList("layout");
}

renderFormats(input, formats, options = {}) {
return this.wrapper.renderInput(input, formats, { engine: "dot", ...options });
}

render(input, options = {}) {
return this.wrapper.renderInput(input, { format: "dot", engine: "dot", ...options });
let format;

if (options.format === void 0) {
format = "dot";
} else {
format = options.format;
}

let result = this.wrapper.renderInput(input, [format], { engine: "dot", ...options });

if (result.status === "success") {
result.output = result.output[format];
}

return result;
}

renderString(src, options = {}) {
41 changes: 36 additions & 5 deletions packages/viz/src/wrapper.mjs
Original file line number Diff line number Diff line change
@@ -32,8 +32,8 @@ class Wrapper {
return list;
}

renderInput(input, options) {
let graphPointer, resultPointer, imageFilePaths;
renderInput(input, formats, options) {
let graphPointer, contextPointer, resultPointer, imageFilePaths;

try {
this.module["agerrMessages"] = [];
@@ -62,19 +62,42 @@ class Wrapper {
this.module.ccall("viz_set_y_invert", "number", ["number"], [options.yInvert ? 1 : 0]);
this.module.ccall("viz_set_reduce", "number", ["number"], [options.reduce ? 1 : 0]);

resultPointer = this.module.ccall("viz_render_graph", "number", ["number", "string", "string"], [graphPointer, options.format, options.engine]);
contextPointer = this.module.ccall("viz_create_context");

if (resultPointer === 0) {
this.module.ccall("viz_reset_errors");

let layoutError = this.module.ccall("viz_layout", "number", ["number", "number", "string"], [contextPointer, graphPointer, options.engine]);

if (layoutError !== 0) {
return {
status: "failure",
output: undefined,
errors: this.#parseErrorMessages()
};
}

let output = {};

for (let format of formats) {
resultPointer = this.module.ccall("viz_render", "number", ["number", "number", "string"], [contextPointer, graphPointer, format]);

if (resultPointer === 0) {
return {
status: "failure",
output: undefined,
errors: this.#parseErrorMessages()
};
} else {
output[format] = this.module.UTF8ToString(resultPointer);

this.module.ccall("free", "number", ["number"], [resultPointer]);
resultPointer = 0;
}
}

return {
status: "success",
output: this.module.UTF8ToString(resultPointer),
output: output,
errors: this.#parseErrorMessages()
};
} catch (error) {
@@ -88,10 +111,18 @@ class Wrapper {
throw error;
}
} finally {
if (contextPointer && graphPointer) {
this.module.ccall("viz_free_layout", "number", ["number"], [contextPointer, graphPointer]);
}

if (graphPointer) {
this.module.ccall("viz_free_graph", "number", ["number"], [graphPointer]);
}

if (contextPointer) {
this.module.ccall("viz_free_context", "number", ["number"], [contextPointer]);
}

if (resultPointer) {
this.module.ccall("free", "number", ["number"], [resultPointer]);
}
1 change: 1 addition & 0 deletions packages/viz/test/manual/instance-reuse.mjs
Original file line number Diff line number Diff line change
@@ -31,6 +31,7 @@ const tests = [
{ label: "string", fn: viz => viz.render(dotStringify(makeObject())) },
{ label: "string with labels", fn: viz => viz.render(dotStringify(makeObjectWithLabels())) },
{ label: "string with HTML labels", fn: viz => viz.render(dotStringify(makeObjectWithHTMLLabels())) },
{ label: "string with multiple formats", fn: viz => viz.renderFormats(dotStringify(makeObject()), ["svg", "cmapx"]) },
{ label: "object", fn: viz => viz.render(makeObject()) },
{ label: "object with labels", fn: viz => viz.render(makeObjectWithLabels()) },
{ label: "object with HTML labels", fn: viz => viz.render(makeObjectWithHTMLLabels()) },
32 changes: 32 additions & 0 deletions packages/viz/test/manual/performance-multiple.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { instance } from "../../src/standalone.mjs";
import { measure, randomGraph, dotStringify } from "./utils.mjs";

const tests = [
{ nodeCount: 100, randomEdgeCount: 10 },
{ nodeCount: 1000, randomEdgeCount: 50 },
{ nodeCount: 1000, randomEdgeCount: 500 },
{ nodeCount: 1000, randomEdgeCount: 1000 }
];

tests.forEach(test => {
test.input = dotStringify(randomGraph(test.nodeCount, test.randomEdgeCount));
});

const timeLimit = 5000;

for (const { input, nodeCount, randomEdgeCount } of tests) {
const viz = await instance();
const result = measure(() => {
viz.render(input, { format: "svg" });
viz.render(input, { format: "cmapx" });
}, timeLimit);
console.log(`render, ${nodeCount} nodes, ${randomEdgeCount} edges: ${result}`);
}

for (const { input, nodeCount, randomEdgeCount } of tests) {
const viz = await instance();
const result = measure(() => {
viz.renderFormats(input, ["svg", "cmapx"]);
}, timeLimit);
console.log(`renderFormats, ${nodeCount} nodes, ${randomEdgeCount} edges: ${result}`);
}
19 changes: 1 addition & 18 deletions packages/viz/test/manual/performance-object.mjs
Original file line number Diff line number Diff line change
@@ -1,22 +1,5 @@
import { instance } from "../../src/standalone.mjs";
import { randomGraph, dotStringify } from "./utils.mjs";

function measure(operation, timeLimit) {
let callCount = 0;

const startTime = performance.now();

while (performance.now() - startTime < timeLimit) {
operation();
callCount++;
}

const stopTime = performance.now();
const duration = (stopTime - startTime) / 1000;
const speed = callCount / duration;

return `${callCount} in ${duration.toFixed(2)} s, ${speed.toFixed(2)} calls/s`
}
import { measure, randomGraph, dotStringify } from "./utils.mjs";

const tests = [
{ nodeCount: 100, randomEdgeCount: 10 },
26 changes: 8 additions & 18 deletions packages/viz/test/manual/performance-timing.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { instance } from "../../src/standalone.mjs";
import { randomGraph, dotStringify } from "./utils.mjs";
import { measure, randomGraph, dotStringify } from "./utils.mjs";

const tests = [
{ nodeCount: 100, randomEdgeCount: 0 },
@@ -13,24 +13,14 @@ const tests = [
{ nodeCount: 100, randomEdgeCount: 300 }
];

tests.forEach(test => {
test.input = dotStringify(randomGraph(test.nodeCount, test.randomEdgeCount));
});

const timeLimit = 5000;

for (const { nodeCount, randomEdgeCount } of tests) {
for (const { input, nodeCount, randomEdgeCount } of tests) {
const viz = await instance();
const src = dotStringify(randomGraph(nodeCount, randomEdgeCount));

let callCount = 0;

const startTime = performance.now();

while (performance.now() - startTime < timeLimit) {
viz.render(src);
callCount++;
}

const stopTime = performance.now();
const duration = (stopTime - startTime) / 1000;
const speed = callCount / duration;

console.log(`${nodeCount} nodes, ${randomEdgeCount} edges: ${callCount} in ${duration.toFixed(2)} s, ${speed.toFixed(2)} calls/s`);
const result = measure(() => viz.render(input), timeLimit);
console.log(`${nodeCount} nodes, ${randomEdgeCount} edges: ${result}`);
}
17 changes: 17 additions & 0 deletions packages/viz/test/manual/utils.mjs
Original file line number Diff line number Diff line change
@@ -1,3 +1,20 @@
export function measure(operation, timeLimit) {
let callCount = 0;

const startTime = performance.now();

while (performance.now() - startTime < timeLimit) {
operation();
callCount++;
}

const stopTime = performance.now();
const duration = (stopTime - startTime) / 1000;
const speed = callCount / duration;

return `${callCount} in ${duration.toFixed(2)} s, ${speed.toFixed(2)} calls/s`
}

const skipQuotePattern = /^([A-Za-z_][A-Za-z_0-9]*|-?(\.[0-9]+|[0-9]+(\.[0-9]+)?))$/;

function quote(value) {
Loading

0 comments on commit 2837bae

Please sign in to comment.