Memory

A viewer for graph files loads graph files. This page is the honest accounting, because “it uses some memory” is not something anyone can plan against.

What a graph costs

Measured on a 133 MB .kgl holding 546,850 nodes, 765,373 edges and 98 node types:

Load time

~0.8 s

Resident after load

~627 MB

Resident after a session that also served several renders

737 MB (measured)

kglite’s temporary spill in $TMPDIR

370 MB

The spill is not optional and not this project’s: kglite writes any column of 256 KB or more to $TMPDIR while decoding. Both show(path) and show(graph) need a writable temporary directory.

Handing a graph over costs twice

kglite_visual.show(graph) on an in-memory kglite KnowledgeGraph costs roughly 2× the graph’s size at the moment of the call: to_bytes() materialises a complete .kgl image in the Python process, and this wheel decodes a second, independent copy inside its own extension module.

That is not an optimisation waiting to happen. Two extension modules cannot share a graph handle, and a serialised image is the only sound handover. show(path) reads the file directly and pays once — so on a large graph, pass the path.

The load ceiling

--max-load-mb N (and show(..., max_load_mb=N)) refuses a graph estimated to cost more than N megabytes, in hundredths of a second, before anything is decompressed. Measured: 0.026 s to refuse a file that takes 0.8 s to load.

The case it exists for is a 16 GB machine that would otherwise spend twenty minutes in swap. The estimate is deliberately conservative and can refuse a graph that would have fitted, which makes it a guard rather than a budget — set it where a failure is what you want.

Full detail: the load ceiling.

Render against a running server, not in a loop

Each kglite-visual render invocation loads the whole graph fresh. A script that renders twenty images of one graph pays that twenty times.

If a server is already up, POST /api/render reuses the graph already in memory and answers with the image bytes. It renders against a private session over the same read-only graph, so asking for an image never moves what the user is looking at.

The path builder is where a query gets expensive

The bound protects the response, not the query’s own execution. A query the engine has to run through before it can return five rows still costs what it costs.

Measured on a 546,850-node graph: a three-hop path previewing at 1,941,015 rows answers in about a second under kglite 0.16.17, truncated to the row ceiling. Reaching the work-unit guard one hop further out cost +2.9 GB of RSS on a query the engine then refused, and a query cancelled at its deadline peaked at 4.9 GB. The server survived every one of those.

Under kglite 0.16.15 and earlier the same three-hop query ran more than 120 seconds past a 30-second deadline and reached 7.3 GB before the OS killed the server — the deadline was polled inside the pattern matcher but not in the row-building layer above it. kglite 0.16.16 fixed that, so a runaway is now a bounded wait; it is not a free one, because the memory a query allocated before it stopped was still allocated.

That is exactly why every hop in the path builder carries a count(*) preview, and why the card warns before the click rather than after the wait. Read the counts.

Shutting down cleanly matters

SIGTERM — which is what kill sends by default — is caught. The server shuts down, exits 0, releases the port, and removes kglite’s temporary spill. Ctrl-C is the same handler, and so is Server.close() from Python.

kill -9 skips all of it and leaves 370 MB behind. Nothing inside a process can prevent that.