Repository navigation
Conversation
RawTable copies every table out of the Resource into a new buffer, and a font.Font keeps the tables it parses. For a color emoji font this is most of the file: the 'sbix' table of macOS Apple Color Emoji.ttc is 182MB, so NewFont on it keeps 193MB on the Go heap. NewLoadersFromBytes builds the loaders from a []byte and returns each uncompressed table as a sub-slice of it, with its capacity limited to the table end. Compressed WOFF tables are still decoded into a new buffer. The input can be a read-only memory mapping, so the OS pages the tables in as they are used and can drop them again under memory pressure. The dst argument of RawTableTo is not used by such a loader: it may be a table returned before, which is then a view of the input. Measured on Apple Color Emoji.ttc, face 0, heap after GC with the Font alive: os.File + NewLoaders +193MB; mmap + NewLoadersFromBytes +11MB. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TestParseCrashers already feeds random input to NewLoaders, which NewLoadersFromBytes calls. The loop also used math/rand.Read, which staticcheck flags (SA1019). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…table at EOF - Doc: the whole input stays reachable while any loader, font or data derived from them is in use; it must not be modified or unmapped; a returned table must never be passed as dst to a copying loader. - An empty table at offset == len(data) is now rejected, as ReadAt does on the copying path. - Tests: a compressed WOFF table with a view of the input as dst (fails without the dst guard); truncated input must accept and reject the same tables as NewLoaders; empty table at EOF. The old dst test is removed: it passed without the guard. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Hello, While I sympathize with the issue, I'm not sure I like the proposed solution that much. It seems the issue is that Could we instead introduce an interface extending Then, the new interface is easy to implement if you already have the whole resource as a byte slice. |
|
Thanks, that is cleaner. I'll rework it as an optional interface checked in
On the slice path, |
Yes, returning an error should be fine. I'm not sure about exporting a concrete type wrapping a slice. Maybe that would benefit other users who already have the full slice "in memory" ? |
|
Thanks. I'll add |
As suggested in review, the zero-copy path is now an optional interface checked in findTableBuffer, and not a separate constructor: - ResourceSlicer is a Resource with Slice(start, end int64) ([]byte, error). When the Resource given to NewLoader(s) implements it, uncompressed tables are returned as the slice from Slice, capped at the table end. dst is not used, since a previous result can be a view of read-only memory. - NewSliceResource([]byte) wraps a bytes.Reader for callers that already have the whole file in memory, or as a memory mapping. - A Slice error, or a slice of the wrong length, is returned as an error. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@benoitkugler The rework is pushed (b157558):
The heap result is the same as before: +11 MB with an mmap of Apple Color Emoji, against +193 MB with an |
|
The It stops before it checks any code. Other branches fail the same way, for example Changing the pin in |
benoitkugler
left a comment
There was a problem hiding this comment.
Thank you for this new version.
I have left two comments I would like to adress before merging, but the direction looks right overall !
| // compare as uint64: int(s.length) can overflow on 32-bit platforms | ||
| if uint64(len(table)) != uint64(s.length) { | ||
| return nil, fmt.Errorf("invalid table: Slice(%d, %d) returned %d bytes", start, end, len(table)) | ||
| } |
There was a problem hiding this comment.
This looks odd to me : I would prefer to move the check (if required) to the ResourceSlicer implementation, and trust its result.
There was a problem hiding this comment.
Done in 9ae34df. The Slice contract now requires exactly end - start bytes, and findTableBuffer returns its result without a check.
| // limit the capacity so that appending to the table can not | ||
| // overwrite the next one | ||
| return table[:len(table):len(table)], nil |
There was a problem hiding this comment.
That seems not required given the contract of ResourceSlicer :
The capacity of the returned table is limited to its length.
There was a problem hiding this comment.
Done in 9ae34df. The capacity limit is now part of the Slice contract, sliceResource.Slice returns data[start:end:end], and the limit in findTableBuffer is removed.
The Slice contract now requires a result of exactly end - start bytes, with capacity limited to its length. sliceResource enforces it, and findTableBuffer returns the result without checks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
This looks good to me. I would love to have the approval of @andydotxyz or @whereswaldon though, since there is an API addition.
andydotxyz
left a comment
There was a problem hiding this comment.
Seems like a good change - the interface API etc is clean (if a little doc heavy) - but the constructor function name doesn't seem right.
| // the operating system loads the tables in memory when they are used. | ||
| // | ||
| // See [ResourceSlicer] for how [data] is used. | ||
| func NewSliceResource(data []byte) ResourceSlicer { |
There was a problem hiding this comment.
This method name does not read well in my mind. Should it be NewSlicedResource - or NewSliceableResource perhaps? open to other suggestions too
There was a problem hiding this comment.
Are you asking the other contributors or me?
There was a problem hiding this comment.
Anyone who can help move it forward I guess
There was a problem hiding this comment.
The name reads fine to me as is. That being said, I'm not married to it. Pick a new one, and I'll change it.
Loader.RawTablecopies every table out of theResourceinto a new buffer, and afont.Fontkeeps the tables it parses. For a color emoji font that is most of the file. Thesbixtable of macOSApple Color Emoji.ttcis 182 MB, soNewFonton it keeps 193 MB on the Go heap. A terminal that falls back to this font for one emoji grows by that much.As suggested in review, this PR adds an optional interface, checked in
findTableBuffer:When the
Resourcegiven toNewLoaderorNewLoadersis aResourceSlicer, each uncompressed table is the slice thatSlicereturns. It is not copied.NewSliceResourcewraps abytes.Readerfor callers that have the whole file in memory. The data can also be a read-only memory mapping of the file. The OS then pages the tables in as they are used, and can drop the pages again under memory pressure.Slicereturns an error, or a slice of the wrong length,RawTablereturns an error.dstinRawTableTois not used.dstcan be a table that an earlier call returned (NewFontandfontscanpass the previous buffer back), and that table is now a view of read-only memory.NewSliceResourcerejects the same table ranges asbytes.Reader.ReadAt, so both paths accept the same fonts.Resourcethat does not implementSliceworks as before.I checked that nothing in
fontortableswrites into a table buffer. The only write found, inKernData1.parseValues, goes to a[4]bytearray field.Measured
Apple Color Emoji.ttc, face 0, heap after
runtime.GC()with theFontalive:os.Filesyscall.Mmap+NewSliceResourceU+1F600still decodes as a PNGGlyphBitmap.Tests
commonandcollectionsfont (this includes a compressed WOFF) reads back the same as from abytes.Reader.cap == len.RawTableTowith the previous table asdstleaves the input unchanged.Sliceerror, or a short slice, from an outside implementation is returned as an error.font,sbixglyph data from the Sbix toy fonts is the same as from abytes.Reader, and it points into the input.go test ./...,go vetandstaticcheckv0.8.2 are clean.🤖 Generated with Claude Code