Skip to content

Resource comparison methodology

Sebastien DUMETZ edited this page Oct 20, 2026 · 1 revision

Resource comparison methodology

The resource usage comparison is a small set of practical observations made on 20 October 2026 to explain objtools' resource tradeoffs. It is not a neutral performance benchmark, a quality ranking, or a claim that these tools perform equivalent work. We maintain objtools and chose workloads that exercise its intended use.

Samples and shortcuts

All four inputs are generated, flat, connected triangle grids in a single OBJ object. Every position is referenced. All faces have the same explicit normal; the textured grids have one UV per position. This exercises a large individual mesh without making normal generation, polygon triangulation or seams dominate the comparison.

The small grid is 100 × 100 positions; the large grid is 2,500 × 4,000. The two textured grids are 316 × 316. One uses a single 16,384 × 16,384 RGB PNG. The other assigns four 8,192 × 8,192 RGB PNGs to successive horizontal material bands. The two texture cases contain the same total pixel count, but each material in the four-image case uses approximately a quarter of its image's UV area.

These are controlled examples, not scans or a representative corpus of users' assets. In particular:

  • The grids have simple topology, no UV seams within a material, and no concave polygons, georeferencing, vertex colors, animation or scene hierarchy. The large case has neither UVs nor textures.
  • The textures are solid colors. They compress extremely well, while retaining large decoded dimensions. They illustrate pixel-area costs, not photographic texture I/O, image quality, alpha scanning or expensive image conversion.
  • All source images are already glTF-compatible PNGs. We did not force transcoding. objtools can copy them without decoding all pixels; forcing image conversion would use heap proportional to a decoded image. See Images.
  • Only four materials are used. Many objects, materials and image records can grow metadata memory; this is not a claim of constant total heap for arbitrary scenes.

Machine and versions

Linux 6.12.96, Intel Core i5-4590 (four cores), 32 GiB physical RAM and 8 GiB configured swap. Inputs, outputs and objtools workspaces are on a Samsung 860 EVO SATA SSD, using Btrfs with compress=zstd:3. /tmp is tmpfs on this machine and was used only for tool installation and the wiki checkout, not for model workspaces. Filesystem compression can especially benefit these repetitive synthetic files.

Tool Version / source
objtools Release build, cargo build --release --locked, source commit 26f04b2.
obj2gltf npm obj2gltf@3.2.0, Node.js 24.2.0, default V8 heap settings.
Blender 5.2.1 LTS, official Linux release, build 9e2066aef7ef.
glTF Transform @gltf-transform/cli 4.5.0, Node.js 24.2.0.
TexMetro Local build from the sdumetz/texmetro fork at e1d6503, not an unmodified upstream release.

Conversion commands

Each tool runs as a fresh process, with a separate destination directory. Installation, input generation and release compilation are excluded. Process startup, OBJ reading, conversion, image handling, output writing and normal shutdown are included.

objtools import sample/mesh.obj -o sample/objtools/mesh.gltf
node path/to/obj2gltf/bin/obj2gltf.js \
  -i sample/mesh.obj -o sample/obj2gltf/mesh.gltf --separate
BLENDER_USER_CONFIG="$PWD/blender-config" blender \
  --background -noaudio --factory-startup --python-exit-code 1 \
  --python blender_convert.py -- sample/mesh.obj sample/blender/mesh.gltf

The Blender script starts with an empty factory scene and calls:

bpy.ops.wm.obj_import(filepath=source, forward_axis="NEGATIVE_Z", up_axis="Y")
bpy.ops.export_scene.gltf(filepath=destination, export_format="GLTF_SEPARATE",
                          export_image_format="AUTO", export_yup=True)

All tools target glTF JSON with external geometry and images. We did not tune each exporter for minimum memory, enable compression, optimize its mesh, or make every material/export option identical. Default export processing may do different work and produce different vertex layouts or material approximations. Successful output counts are checked against the source triangle counts; this is not a visual fidelity evaluation or exhaustive output validation.

Blender initially exported the small sample but hung during sandbox shutdown with a PulseAudio permission error. That attempt and an audio-disabled sandbox retry are excluded; the recorded Blender runs use the normal host environment and complete shutdown, except the explicitly stopped large run.

The small objtools and obj2gltf conversions were recorded inside that sandbox and completed without errors. The remaining ordinary runs use the host environment. This is another reason to treat the timings as illustrations rather than controlled performance rankings.

Inspection commands and scope

objtools and glTF Transform inspect the same glTF produced by objtools, to avoid comparing different export layouts. This means OBJ conversion is excluded from those inspection times. For an end-to-end OBJ inspection, objtools must also perform a temporary import; do not apply the glTF-only times directly to OBJ input.

objtools inspect sample/objtools/mesh.gltf
node path/to/@gltf-transform/cli/bin/cli.js inspect sample/objtools/mesh.gltf
texmetro sample/mesh.obj --json sample/texmetro.json

The reported TexMetro rows use the source-linked build above, with QT_QPA_PLATFORM=offscreen and QT_IMAGEIO_MAXALLOC=2048 (MiB). An initial run of an installed binary with unknown exact revision rejected the 16k PNG at Qt's default 256 MiB image allocation limit, after about 24 seconds and 4.2 GiB peak RSS. Its four-image case completed. Both initial observations are retained in the records, but the table uses the subsequent source-linked build for both texture cases. Raising the image allocation limit allowed the 16k case to complete. We did not record the OpenGL renderer/backend; do not read these timings as portable GPU-performance figures.

For interactive use, the glTF Transform command can also be invoked as npx --package=@gltf-transform/cli gltf-transform inspect model.gltf. The measurements call the already-installed CLI directly, excluding npm resolution, downloads and wrapper overhead.

glTF Transform's inspection report covers scenes, meshes, materials, textures and animations, including resource-size estimates. Those size estimates are not measurements of the inspector's own peak memory. It does not compute the texture occupancy measured by objtools.

TexMetro computes topology, atlas, distortion, seam and raster metrics, including multiple mip levels. objtools measures base-level sampled-texel coverage with filtering and wrapping. TexMetro's full analysis does more work and its occupancy need not equal objtools' figure. The two untextured, UV-free grids are omitted from the TexMetro runs because they do not supply the parameterization required for its texture metrics.

Memory, timing and stopping rules

Runs are sequential, one observation per case and tool. There is no cache flushing, randomized ordering, CPU isolation or repeated-run statistics. The machine is a normal workstation with other applications running. Cache state, filesystem writeback and system load affect elapsed time. We do not wait for output bytes to be durably flushed to the SSD. Small timing differences have little significance.

A Linux harness samples /proc/PID/status every 100 ms and collects each child's peak RSS with wait4().ru_maxrss. Elapsed time has up to roughly one polling interval of observation overhead, so sub-100-ms runs appear as approximately 0.1 seconds. For very short processes, sampled anonymous memory can miss the workload entirely. Process peaks include startup, shared libraries and the launcher's transient pre-exec memory; GPU memory and a whole process-tree aggregate are not measured.

RSS combines anonymous memory and resident mapped file pages. objtools' file pages are backed by workspace files and can be reclaimed by the kernel; its unconstrained RSS is not its minimum RAM requirement. The separate anonymous measurement is sampled, not an exact heap high-water mark. Neither RSS nor anonymous memory alone measures all machine memory used by a run: buffered file I/O also uses the kernel page cache.

The ordinary-run harness kills its own process group if sampled RSS exceeds 8 GiB or runtime exceeds 600 seconds, to keep the workstation usable. A stopped run is an incomplete observation, not evidence that the tool cannot finish with more resources. Peaks may overshoot the RSS guard between samples. The limit is the same for each tool, but no effort was made to find the minimum successful memory budget for each.

Constrained-memory check: failures included

Three additional objtools runs use systemd cgroups. Each has MemorySwapMax=0 and a 300-second timeout. The cgroup contains the importer and its Python measurement wrapper, and caps the service's accounted memory including its charged file cache, while leaving the rest of the workstation unconstrained. Input, output and workspace files remain on the SSD.

Settings Outcome
MemoryMax=256M OOM-killed during spooling after 6.5 seconds; service memory peak 256 MiB.
MemoryMax=512M OOM-killed during spooling after 12 seconds; service memory peak 512 MiB.
MemoryHigh=384M, MemoryMax=512M Completed in 49 seconds; service memory peak 386 MiB, no swap; importer peak RSS 260 MiB, sampled anonymous peak about 1 MiB.

The final run introduces a lower soft threshold, which applies memory-pressure throttling/reclaim before the hard ceiling. It produced all 19,987,002 triangles. A low importer heap did not make the plain hard-cap runs succeed. We have not isolated the precise filesystem/kernel cause of those OOM kills, nor searched for a minimum viable budget. These results support the specific configured capacity example, not a blanket promise that 512 MiB always suffices.

To reproduce the successful configuration after generating large/mesh.obj and setting the tool paths described below:

systemd-run --user --wait --pipe \
  -p MemoryHigh=384M -p MemoryMax=512M -p MemorySwapMax=0 \
  -p RuntimeMaxSec=300 -p WorkingDirectory="$PWD" \
  --setenv=OBJTOOLS="$OBJTOOLS" \
  /usr/bin/python3 limited.py 512m-high

The host still has 32 GiB RAM: we did not physically replace it with a 512 MiB machine or measure every kernel allocation and pre-existing shared cache page. The 49-second run is a capacity check, not a second speed comparison. We did not rerun the other tools under these cgroup settings.

Reproduction and records

The wiki's Git repository contains the generator, runner, Blender script and recorded results under measurements/2026-10-20/. Clone git@github.com:sdumetz/objtools.wiki.git to retrieve them. The generated meshes and textures are not committed. The main objtools repository contains only the README link; local measurement work is kept in its ignored out/ directory.

Copy the measurement scripts into an empty directory on a real disk, install Python's Pillow package, and install the Node tools before measuring:

npm install obj2gltf@3.2.0 @gltf-transform/cli@4.5.0
export OBJTOOLS=/absolute/path/to/objtools/target/release/objtools
export TEXMETRO=/absolute/path/to/source-linked/texmetro
python3 generate.py
python3 run.py

The runner also accepts NODE, OBJ2GLTF_CLI and GLTF_TRANSFORM_CLI environment variables. Use a fresh working directory for a fresh suite: existing reports are deliberately skipped, and objtools refuses to overwrite existing output. At least several GiB of workspace space are needed; leave ample extra room for interrupted imports. Script tool paths are configurable in the published copy, which uses the final TexMetro image-limit setting; recorded commands replace the original local absolute paths with placeholders. The records include output counts, file sizes, hashes and unsuccessful attempts. No repeated-run statistics or raw process-memory traces were collected.

Resource usage comparison · How it works

Clone this wiki locally