Skip to content

Share images referenced several times and decode them once per render… - #1159

Open
gorghoa wants to merge 1 commit into
linebender:mainfrom
gorghoa:usvg-share-images
Open

gorghoa wants to merge 1 commit into
linebender:mainfrom
gorghoa:usvg-share-images

Conversation

@gorghoa

@gorghoa gorghoa commented Oct 12, 2026

Copy link
Copy Markdown
Contributor

Addresses #1147.

An image drawn several times through was loaded again by usvg for every instance (data URL decoded again, file read again, nested SVG parsed again), so each instance got its own data buffer, and resvg then decoded each instance again.

usvg: the resolved xlink:href of image and feImage elements is cached in converter::Cache, keyed by the href string, so all instances of an image share the same Arc data.

resvg: render::Context gets a cache of decoded raster images, keyed by the data pointer. An image is kept decoded only when its data has more than one owner (Arc::strong_count > 1), so images drawn once use no more memory than before.

Tests

  • All 1731 integration tests and the usvg tests pass, unchanged.
  • New usvg tests check that every instance of an image, and every image with the same href, gets the same Arc data , and that an image used once has no other owner once parsing is done (resvg relies on this to decide which decoded images to keep).
  • Output is byte-identical to main on every file below.

Benchmarks

resvg CLI, release build, Linux x86_64, best of 5 runs, peak RSS from wait4(2). One 4000x3000 JPEG (1.5 MB) embedded as a data URL:

File main this commit
drawn once 59 ms, 55 MB 58 ms, 55 MB
drawn 10x via 509 ms, 69 MB 67 ms, 55 MB
drawn 30x via 1517 ms, 100 MB 93 ms, 56 MB
10 distinct JPEGs 590 ms, 87 MB 596 ms, 86 MB

The usvg change alone only saves the data URL decoding (30x: about 1.5 s -> 1.4 s on a noisier run); most of the gain comes from resvg decoding once.

Disclosure: sorry but this change was written end to end by an LLM (Claude opus 5.5).

… (exploratory, LLM-generated)

Addresses linebender#1147. An image drawn several times through <use> was loaded
again by usvg for every instance (data URL decoded again, file read
again, nested SVG parsed again), so each instance got its own data
buffer, and resvg then decoded each instance again.

usvg: the resolved `xlink:href` of `image` and `feImage` elements is
cached in `converter::Cache`, keyed by the href string, so all instances
of an image share the same `Arc` data.

resvg: `render::Context` gets a cache of decoded raster images, keyed by
the data pointer. An image is kept decoded only when its data has more
than one owner (`Arc::strong_count > 1`), so images drawn once use no
more memory than before.

Tests
-----

- All 1731 integration tests and the usvg tests pass, unchanged.
- New usvg tests check that every <use> instance of an image, and every
  image with the same href, gets the same Arc data (both fail on main),
  and that an image used once has no other owner once parsing is done
  (resvg relies on this to decide which decoded images to keep).
- Builds with and without the `raster-images` feature, `cargo fmt
  --check` is clean. Clippy not run.
- Output is byte-identical to `main` on every file below.

Benchmarks
----------

resvg CLI, release build, Linux x86_64, best of 5 runs, peak RSS from
wait4(2). One 4000x3000 JPEG (1.5 MB) embedded as a data URL:

| File                 | main            | this commit     |
|----------------------|-----------------|-----------------|
| drawn once           |    59 ms, 55 MB |    58 ms, 55 MB |
| drawn 10x via <use>  |   509 ms, 69 MB |    67 ms, 55 MB |
| drawn 30x via <use>  | 1517 ms, 100 MB |    93 ms, 56 MB |
| 10 distinct JPEGs    |   590 ms, 87 MB |   596 ms, 86 MB |

The usvg change alone only saves the data URL decoding (30x: about
1.5 s -> 1.4 s on a noisier run); most of the gain comes from resvg
decoding once.

Assumptions: a custom `ImageHrefResolver` returns the same result for
the same href during one parse; if an API user holds an extra clone of
an image's data, an image drawn once is also kept decoded for the
render (memory only, not correctness).

Disclosure: this change was written end to end by an LLM (Claude), as
an exploration. It is published as inspiration only, not as a
contribution, per the Linebender LLM contribution policy.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant