As per https://pkg.go.dev/math/rand#Seed:
"If Seed is not called, the generator is seeded randomly at program startup."
"Prior to Go 1.20, the generator was seeded like Seed(1) at program startup. To
force the old behavior, call Seed(1) at program startup."
"As of Go 1.20 there is no reason to call Seed with a random value."
rand.Seed() is deprecated. Followup to #11015.
PiperOrigin-RevId: 685052229
Earlier wire.Uint.save() could make up to 10 Write() calls depending on how
large the Uint being marshaled was. Write() is an interface method call, so
this avoids the dynamic dispatch overhead. Furthermore, compressio's Write
implementations themselves do a bunch of fixed work per call and invoke more
interface functions.
Increased the scratch buffer in the wire.Writer to accommodate 10 bytes. The
wire.Uint can be marshaled into at most 10 bytes.
Before:
goos: linux
goarch: amd64
pkg: pkg/state/wire/wire
cpu: AMD EPYC 7B12
BenchmarkUintSave
BenchmarkUintSave-24 37557860 31.13 ns/op 0 B/op 0 allocs/op
After:
goos: linux
goarch: amd64
pkg: pkg/state/wire/wire
cpu: AMD EPYC 7B12
BenchmarkUintSave
BenchmarkUintSave-24 129611625 9.274 ns/op 0 B/op 0 allocs/op
PiperOrigin-RevId: 672679703
This change optimizes compressio.SimpleWriter by buffering the output manually.
When a key is provided, SimpleWriter was adding a 4 byte header for each chunk
being written. But in practice, the wire package always calls
SimpleWriter.Write with 1-size byte slices. So each chunk is only 1 in length.
So 80% of the statefile ends up being just chunk headers and 20% is data.
This does the same optimization as 68f0b41bf9 ("compressio: Remove chunk size
from the wire format for SimpleRW when key=nil.") for key!=nil case.
This change additionally optimizes the calls to hash.Hash.Sum() to use existing
scratch buffers and hence avoids a byte-slice allocation.
Before:
BenchmarkTinyIO
BenchmarkTinyIO/NoCompressWriteNoHash1024KbBlock
BenchmarkTinyIO/NoCompressWriteNoHash1024KbBlock-24 100000000 11.76 ns/op 2 B/op 0 allocs/op
BenchmarkTinyIO/NoCompressWriteHash1024KbBlock
BenchmarkTinyIO/NoCompressWriteHash1024KbBlock-24 442598 2738 ns/op 211 B/op 4 allocs/op
After:
BenchmarkTinyIO
BenchmarkTinyIO/NoCompressWriteNoHash1024KbBlock
BenchmarkTinyIO/NoCompressWriteNoHash1024KbBlock-24 100000000 11.22 ns/op 2 B/op 0 allocs/op
BenchmarkTinyIO/NoCompressWriteHash1024KbBlock
BenchmarkTinyIO/NoCompressWriteHash1024KbBlock-24 89841070 16.09 ns/op 3 B/op 0 allocs/op
Co-authored-by: Jamie Liu <jamieliu@google.com>
PiperOrigin-RevId: 672619452
This change optimizes checkpoint/restore when --compression=none is being used.
Note that runsc never uses a key in SimpleRW.
In practice, the SimpleRW structs are only used for checkpoint/restore.
The caller of the Read/Write methods is the wire package. All the types defined
in the wire package (except String and Ref) translate their save()/load()
implementations to wire.Uint.save()/load().
wire.Uint attempts to be smart and compress the uint64 by reading or writing it
out byte by byte using a particular format (where there MSB indicates whether
more bits are needed to construct this uint64).
So what ends up happening is, the entire kernel is serialized byte by byte
and compressio mostly receives one byte slices to read/write.
For each call to Read/Write in SimpleRW, it adds a 4 byte header representing
"chunk size". So we have 4 bytes for chunk size, followed by 1 byte of data.
This is atrociously wasteful. 80% of the checkpoint file is such chunk sizes.
We only require such chunk size headers when a key is provided to compressio
and there is a hash appended after each chunk. So the chunk size would be
needed to figure out where the data ends and the has begins. But in runsc, key
is never used. So this change gets rid of chunk size from the wire format of
SimpleRW when key=nil.
This should reduce the checkpoint.img file size by 80% and speed up kernel
save and load.
PiperOrigin-RevId: 669404610
The 4-byte scratch buffer variables in SimpleWriter.Write and SimpleReader.Read
were being moved to heap because they were being passed into interface methods.
Add a scratch buffer field to SimpleWriter and SimpleReader and use that.
PiperOrigin-RevId: 669036236
These interfaces only existed to add ReadByte() and WriteByte() methods. There
were only 4 implementors of these methods: compressio.{Simple}{Reader/Writer}.
And there were only 2 users of this.
Using io.{Reader/Writer} is more extendible. For instance, it allows using
*os.File with the `wire` package without any wrappers.
Updated the 2 users to implement their own {read/write}Byte(). To avoid heap
allocation of the [1]byte storage during call to io.Reader.Read or
io.Writer.Write due to interface call, used sync.Pool. Earlier, calls to
compressio.Simple{Reader/Writer}'s implementation of {Read/Write}Byte would
cause a heap allocation.
PiperOrigin-RevId: 625167495
IIUC, Go sets testing.b.N by running benchmarks repeatedly with increasing
values of b.N until a "reasonable" benchmark run time is achieved. Before this
CL, decompression benchmarks measure the time to perform *compression* once
while scaling the number of decompressions with b.N, making the Go-observed
benchmark time independent of b.N and causing Go to increase b.N to
unpredictable and unreasonable amounts:
```
BenchmarkDecompressNoHash1M
pkg/compressio/compressio_test.go:152: compress=false, hash=false, len(data)=838860800, blockSize=1048576: compression time 821.474577ms, ratio 0.75, decompression time 1.161417487s
pkg/compressio/compressio_test.go:152: compress=false, hash=false, len(data)=838860800, blockSize=1048576: compression time 526.175362ms, ratio 0.75, decompression time 1.937750371s
pkg/compressio/compressio_test.go:152: compress=false, hash=false, len(data)=838860800, blockSize=1048576: compression time 767.402422ms, ratio 0.75, decompression time 2.156883246s
pkg/compressio/compressio_test.go:152: compress=false, hash=false, len(data)=838860800, blockSize=1048576: compression time 779.148676ms, ratio 0.75, decompression time 2.396898003s
pkg/compressio/compressio_test.go:152: compress=false, hash=false, len(data)=838860800, blockSize=1048576: compression time 577.573005ms, ratio 0.75, decompression time 3.266100928s
pkg/compressio/compressio_test.go:152: compress=false, hash=false, len(data)=838860800, blockSize=1048576: compression time 524.66472ms, ratio 0.75, decompression time 5.922212382s
pkg/compressio/compressio_test.go:152: compress=false, hash=false, len(data)=838860800, blockSize=1048576: compression time 714.897653ms, ratio 0.75, decompression time 13.105200153s
pkg/compressio/compressio_test.go:152: compress=false, hash=false, len(data)=838860800, blockSize=1048576: compression time 768.187436ms, ratio 0.75, decompression time 19.574360494s
pkg/compressio/compressio_test.go:152: compress=false, hash=false, len(data)=838860800, blockSize=1048576: compression time 625.937757ms, ratio 0.75, decompression time 30.097482785s
pkg/compressio/compressio_test.go:152: compress=false, hash=false, len(data)=838860800, blockSize=1048576: compression time 767.470509ms, ratio 0.75, decompression time 56.130162322s
pkg/compressio/compressio_test.go:152: compress=false, hash=false, len(data)=838860800, blockSize=1048576: compression time 527.026934ms, ratio 0.75, decompression time 1m36.04235679s
pkg/compressio/compressio_test.go:152: compress=false, hash=false, len(data)=838860800, blockSize=1048576: compression time 943.736532ms, ratio 0.75, decompression time 3m15.294992151s
panic: SIGTERM
```
PiperOrigin-RevId: 615249406
compressio segments uncompressed data into chunks of `pool.chunkSize` each. In
each call to `Writer.Write()`, any part of the input that spans a full chunk is
passed without copying as an "inline buffer" to the worker pool, while parts of
the input that do not fill up a chunk are copied into temporary buffer
`pool.buf`, hereafter a "pooled buffer" since these are obtained from a global
`bufPool`. Pooled buffers are returned to `bufPool` once they are no longer
needed (i.e. once they have been compressed) to reduce heap allocations; doing
the same for inline buffers would be incorrect since their slices are still
owned by `Writer.Write()`'s caller.
`Writer.Write()` waits for compression of inline buffers to be completed (since
`io.Writer.Write(p)` is forbidden from retaining `p`), but does not wait for
compression of pooled buffers. Thus, at the beginning of any call to
`Writer.Write()`, there may be any number of pooled buffers queued for
compression, but no inline buffers. `Writer.Write()` uses this property to
distinguish between pooled and inline buffers without explicitly marking them
by counting existing pooled buffers as `pendingPre`, and queued inline buffers
as `pendingInline`.
The bug occurs when a call to `Writer.Write()` initially observes a non-empty
`pool.buf` (due to a previous call to `Write()`), and fills it completely with
at least `pool.chunkSize` more data to go (so `pool.buf`, followed by at least
one inline buffer, need to be enqueued). In this case, the enqueued pooled
buffer is not counted in `pendingPre` or `pendingInline`, so `Writer.Write()`
will incorrectly treat the pooled buffer as inline and forget about the last
inline buffer; thus the *following* call to `Writer.Write()` or
`Writer.Close()` will incorrectly treat the last inline buffer as pooled and
"return" it to `bufPool`, allowing the input slice's data to be corrupted by
reuse after the call to `Writer.Write()` has returned.
Fix this by counting such initial pooled buffers in `pendingPre`. Also
restructure the `callback` to hopefully make what is going on a little clearer.
(Unfortunately the panic does not catch this bug, since `Writer.Write()` stops
calling `pool.schedule()` once `pendingInline` reaches 0.)
PiperOrigin-RevId: 611521099
It is an idea of running codespell as part of our presubmit checks.
Before enabling it for new changes, let's fix what it has found.
Signed-off-by: Andrei Vagin <avagin@gmail.com>
checkpoint image compression (compressio) implies additional overhead
during its operations. when gvisor restores the kernel state inflate()
algorithm requires:
- CPU to un/compress the data
- Memory blocks to store and un/compress data
memory blocks originate from the bytes.Buffers and the sync.Pool that
tries to reuse them. they are released only when the system decides it
is a good moment:
pool.go: runtime_registerPoolCleanup(poolCleanup)
in my system (and in production) it takes around 240s to get the related
memory region freed (unmapped()).
during that period of time from the image state is read and the kernel is
loaded till the moment when the `poolCleanup` is called + GC() releasing
buffers gVisor Kernel (sandbox) process holds tens and hundreds of
megabytes of anonymous memory pages (RAM) busy (allocated+reserved).
pretty much often, the memory overhead of using compression can result in x2
memory overhead in production system with checkpoints restore and +100ms
(hundreds) ms of startup latency just to uncompress the image.
our use case does not suffer from having uncompressed images on disk but
suffer from the waste of memory during startup and CPU overhead.
this patch adds flag to disable compression for containers checkpoints.
Signed-off-by: Ivan Prisyazhnyy <john.koepi@gmail.com>
Previously, it was not possible to encode/decode an object graph which
contained a pointer to a field within another type. This was because the
encoder was previously unable to disambiguate a pointer to an object and a
pointer within the object.
This CL remedies this by constructing an address map tracking the full memory
range object occupy. The encoded Refvalue message has been extended to allow
references to children objects within another object. Because the encoding
process may learn about object structure over time, we cannot encode any
objects under the entire graph has been generated.
This CL also updates the state package to use standard interfaces intead of
reflection-based dispatch in order to improve performance overall. This
includes a custom wire protocol to significantly reduce the number of
allocations and take advantage of structure packing.
As part of these changes, there are a small number of minor changes in other
places of the code base:
* The lists used during encoding are changed to use intrusive lists with the
objectEncodeState directly, which required that the ilist Len() method is
updated to work properly with the ElementMapper mechanism.
* A bug is fixed in the list code wherein Remove() called on an element that is
already removed can corrupt the list (removing the element if there's only a
single element). Now the behavior is correct.
* Standard error wrapping is introduced.
* Compressio was updated to implement the new wire.Reader and wire.Writer
inteface methods directly. The lack of a ReadByte and WriteByte caused issues
not due to interface dispatch, but because underlying slices for a Read or
Write call through an interface would always escape to the heap!
* Statify has been updated to support the new APIs.
See README.md for a description of how the new mechanism works.
PiperOrigin-RevId: 318010298
* Rename syncutil to sync.
* Add aliases to sync types.
* Replace existing usage of standard library sync package.
This will make it easier to swap out synchronization primitives. For example,
this will allow us to use primitives from github.com/sasha-s/go-deadlock to
check for lock ordering violations.
Updates #1472
PiperOrigin-RevId: 289033387
Based on the guidelines at
https://opensource.google.com/docs/releasing/authors/.
1. $ rg -l "Google LLC" | xargs sed -i 's/Google LLC.*/The gVisor Authors./'
2. Manual fixup of "Google Inc" references.
3. Add AUTHORS file. Authors may request to be added to this file.
4. Point netstack AUTHORS to gVisor AUTHORS. Drop CONTRIBUTORS.
Fixes#209
PiperOrigin-RevId: 245823212
Change-Id: I64530b24ad021a7d683137459cafc510f5ee1de9
Nothing reads them and they can simply get stale.
Generated with:
$ sed -i "s/licenses(\(.*\)).*/licenses(\1)/" **/BUILD
PiperOrigin-RevId: 231818945
Change-Id: Ibc3f9838546b7e94f13f217060d31f4ada9d4bf0