Commit Graph

436 Commits (c9337022a0ee9d5e2135a49a49d9ddc593618bb9)
 

Author SHA1 Message Date
Daniel Martí c9337022a0 bump gotip and x/exp, mention 1.19 in the changelog 2 years ago
Daniel Martí f37561589b properly quote the path to garble in -toolexec
If we don't quote it, paths containing spaces or quote characters will
fail. For instance, the added test without the fix fails:

        > env NAME='with spaces'
        > mkdir $NAME
        > cp $EXEC_PATH $NAME/garble$exe
        > exec $NAME/garble$exe build main.go
        [stderr]
        go tool compile: fork/exec $WORK/with: no such file or directory
        exit status 1

Luckily, the fix is easy: we bundle Go's cmd/internal/quoted package,
which implements a QuotedJoin API for this very purpose.

Fixes #544.
2 years ago
Daniel Martí b3e6b7a93a prepare changelog for v0.7.0
I'll probably do the release next week. Get ready for it.
2 years ago
Daniel Martí f0d79a38d4 remove a couple of easy TODOs
First, I tried to follow my own past advice to only set GarbleActionID
if ToObfuscate is true. However, that broke at least three parts of
transformCompile, as the hash is used for more than I recalled.
Give up on that idea, because the current code is working as intended.
Better document what GarbleActionID is and what we use it for.

Second, now that https://go.dev/cl/348741 was shipped with Go 1.18,
using the logger when its output is io.Discard is already a no-op.
So we no longer need our debugf wrapper to apply the no-op logic.
2 years ago
Daniel Martí d2a2f2012b obfuscate a few more std packages
First, two cleanups: unsafe and internal/abi are already in
runtimeAndDeps, so they are already not being obfuscated.
No need to repeat them in the other map.

Then, via trial and error, remove:

* runtime/pprof; it seems like we handle its runtime linknames well now.

* os/signal; unclear what "rebuilds don't work" meant, but it works now.
  Our gogarble.txt test already does a full reproducible rebuild.

* crypto/x509/internal/macos; another linkname user that works now.

It is likely that we could remove one or two more packages already,
but it's best to move slowly and watch out for unexpected regressions.
2 years ago
Daniel Martí 9fc19e8bdf support import paths ending with ".go"
When splitFlagsFromFiles saw "-p foo/bar.go",
it thought that was the first Go file, when in fact it's not.
We didn't notice because such import paths are pretty rare,
but they do exist, like github.com/nats-io/nats.go.

Before the fix, the added test case fails as expected:

	> garble build -tags buildtag
	[stderr]
	# test/main/goextension.go
	open test/main/goextension.go: no such file or directory

We could go through the trouble of teaching splitFlagsFromFiles about
all of the flags understood by the compiler and linker, but that feels
like far more code than the small alternative we went with.
And I'm pretty sure the alternative will work pretty reliably for now.

Fixes #539.
2 years ago
Daniel Martí 201d890430 start checking some third party builds for regressions
Our tests should already be pretty extensive,
and any bug fixes should result in more regression test cases,
but testing against a few diverse and popular third party modules
will help prevent unintended regressions while developing garble.

The list is short for now. More can be added later.
This adds protobuf and wireguard from the original issue,
but not cobra and logrus, as they aren't particularly complex nor add
significant variety on top of protobuf and wireguard.

While here, we remove the job that only runs crlf-test.sh,
as we don't really need a separate job for a tiny script.

Fixes #240.
2 years ago
Daniel Martí 7fb390e403 fix support with the latest Go master version
It added packages which are only built with the boringcrypto build tag,
so trying to `go list` them will fail even though it doesn't matter.

While here, a few more minor cleanups:

1) Hide GarbleActionID and ToObfuscate from encoding/json, so that they
   can't possibly collide with the fields consumed from `go list -json`.

2) Add test cases for `garble build` with packages that fail to load.
   Note that this requires GOGARBLE=* to avoid its "does not match any
   package to be built" error.

3) Remove the last use of interface{}, in a testdata file.

Fixes #531.
2 years ago
lu4p 84ba444b7c
Disable seed obfuscator (#535)
The seed obfuscator uses a type declaration in order to declare a function,
which returns a function with the same type.

This breaks when obfuscating literals inside generic functions, because
type declarations inside generic functions are not currently supported.

Therefore the obfuscator gets disabled until
https://github.com/golang/go/issues/47631 is fixed.
2 years ago
Daniel Martí 14d3803a7b fix hashing of generic field names
Trying to make Go master work, I noticed that crypto/tls still failed to
build. The reason was generic structs; we would badly obfuscate their
field names when the types are instantiated:

	> garble build
	[stderr]
	# test/main
	Z4ZpcbMj.go:4: unknown field 'FOpszkrN' in struct literal of type SYdpWfK5[string]
	Z4ZpcbMj.go:5: m8hLTotb.FypXrbTd undefined (type SYdpWfK5[string] has no field or method FypXrbTd)
	exit status 2

See the added comment for what happened and how we fixed it. And add tests.
2 years ago
Daniel Martí 1a3d7868d9 make "garble version" friendlier for devel builds
The proposal at https://go.dev/issue/50603 has been approved,
so Go will at some point start producing module pseudo-versions
even if the main module was built from a VCS clone.

To not wait until a future release like Go 1.20,
implement that ourselves with the help of module.PseudoVersion.

The result is a friendlier version output; what used to be

	$ go install && garble version
	mvdan.cc/garble (devel)

	Build settings:
	[...]

will now look like

	$ go install && garble version
	mvdan.cc/garble v0.0.0-20220505210747-22e3d30216be

	Build settings:
	[...]

Note that we don't use VCS tags in any way, so the prefix is hard-coded
as v0.0.0. That seems fine for development builds, and Go doesn't embed
VCS tag information in binaries anyway.

Finally, note that we start printing the module sum, as it's redundant.
The VCS commit hash, at least in git, should be unique enough.
2 years ago
Daniel Martí 61bd95bb89 add a test with generic code
This ensures that we support obfuscating builds containing the use of
type parameters, the new feature in Go 1.18.
The test is small for now, but we can extend it over time.

There was just one bug that kept the code from obfuscating properly;
that has been fixed in https://go.dev/cl/405194,
and we update x/tools to the latest master version to include it.

Fixes #414.
2 years ago
shellhazard 22e3d30216
support code taking the address of a []byte literal (#530) 2 years ago
Daniel Martí 6118c795c6 various version bumps
Use a gotip version that's a month newer.

Update to the latest staticcheck version, which supports Go 1.18.

Update all direct dependencies as well.
2 years ago
Daniel Martí 06d2f2d561 avoid capturing groups in regular expressions
We don't use sub-matches captured by these groups,
so avoiding that extra work will save some CPU cycles.
It is likely insignificant compared to the rest of a Go build,
but it's a very easy little win.
2 years ago
Daniel Martí 79b6e4db3d skip unnecessary "refusing to list" test errors
The added test case reproduces the failure if we uncomment the added
"continue" line in processImportCfg:

	# test/bar/exporttest [test/bar/exporttest.test]
	panic: refusing to list non-dependency package: test/bar/exporttest

	goroutine 1 [running]:
	mvdan.cc/garble.processImportCfg({0xc000166780?, 0xc0001f4a70?, 0x2?})
		/home/mvdan/src/garble/main.go:983 +0x58b
	mvdan.cc/garble.transformCompile({0xc000124020?, 0x11?, 0x12?})
		/home/mvdan/src/garble/main.go:736 +0x338

It seems like a quirk of cmd/go that it includes a redundant packagefile
line in this particular edge case, but it's generally harmless for "go
build". For "garble build" it's also harmless in principle, but in
practice we had sanity checks that got upset by the unexpected line.

For now, notice the edge case and ignore it.

Fixes #522.
2 years ago
Daniel Martí e3a59eae07 add missing context to two unmarshal errors
Returning a json or gob error directly to the user is generally not
helpful, as it lacks any form of context.
For example, from the json unmarshal of "go env -json":

	$ garble build
	invalid character 'w' looking for beginning of value

Also improve the error when the user ran garble in the wrong way,
resulting in no shared gob file. The context is now shorter, and we also
include the os.Open error in case it contains any useful details.

While here, apply Go tip's gofmt, which reformatted a godoc list.

For #523.
2 years ago
Daniel Martí 20ff64128e make KnownReflectAPIs aware of variadic funcs
When a function definition is variadic,
the number of parameters may not match the number of calling arguments.
Our existing code was a bit naive with this edge case,
leading to a panic when indexing call.Args.

Keep track of that information, and properly handle all variadic
arguments that may be used indirectly with reflection.

We add a test case that used to panic, where 0 arguments are used for a
variadic parameter, as well as a case where we need to disable
obfuscation for a Go type used as the second variadic argument rather
than the first.

Finally, the old code in findReflectFunctions looked at Go function
types from the point of view of go/ast.FuncType.
Use go/types.Signature instead, where we don't need to deal with the
grouping of variables in the original syntax, and which is more
consistent with the rest of the garble codebase.

Fixes #524.
2 years ago
pagran c8e0abf9c9 Fix removing named imports and fix removing imports with init() methods 2 years ago
Daniel Martí 6a39ad2d81 make "garble version" include VCS information
When someone builds garble from a git clone,
the resulting binary used to not contain any information:

	$ garble version
	(devel)

Since Go 1.18, VCS information is stamped by default into binaries.
We now print it, alongside any other available build settings:

	$ garble version
	mvdan.cc/garble (devel)

	Build settings:
	       -compiler gc
	     CGO_ENABLED 1
	          GOARCH amd64
	            GOOS linux
	         GOAMD64 v3
	             vcs git
	    vcs.revision 91ea246349
	        vcs.time 2022-03-18T13:45:11Z
	    vcs.modified true

Note that it's still possible for a garble build to contain no useful
version information, such as when built via "go build -buildvcs=false".
However, if a user opts into omitting the information, it's on them to
figure out what version of garble they actually built.

While here, bump test-gotip.

Fixes #491.
2 years ago
Daniel Martí 1c564ef091 slightly improve code thanks to Go 1.18 APIs
strings.Cut makes some string handling code more intuitive.
Note that we can't use it everywhere, as some places need LastIndexByte.

Start using x/exp/slices, too, which is our first use of generics.
Note that its API is experimental and may still change,
but since we are not a library, we can control its version updates.

I also noticed that we were using TrimSpace for importcfg files.
It's actually unnecessary if we swap strings.SplitAfter for Split,
as the only whitespace present was the trailing newline.

While here, I noticed an unused copy of printfWithoutPackage.
2 years ago
lu4p 237e0b7b7c Detect unnecessary imports instead of hardcoding 2 years ago
lu4p 1a0b028db7 all: drop support for Go 1.17
Now that we've released v0.6.0, that will be the last feature release to
feature support for Go 1.17. The upcoming v0.7.0 will be Go 1.18+.

Code-wise, the cleanup here isn't super noticeable,
but it will be easier to work on features like VCS-aware version
information and generics support without worrying about Go 1.17.
Plus, now CI is back to being much faster.

Note how "go 1.18" in go.mod makes "go mod tidy" more aggressive.
2 years ago
lu4p d555639657 Remove unused imports via go/types.
Fixes #481
2 years ago
Daniel Martí 8e7765ee6f CONTRIBUTING: introduce -toolexec and add more dev tips
I've explained this before, such as over chat or talks with slides,
so it's time to have a proper version in our docs.

It doesn't really belong in the README, because a user shouldn't have to
understand these internal details.

The way Go builds with -toolexec spawn sub-processes is a bit tricky,
so I've attempted to demonstrate it with a vertical tree using Unicode.
Each added indentation level is one more sub-process level.
2 years ago
Daniel Martí 0b6769c807 remove duplicate go:generate directive
I hadn't noticed that cmd/bundle prints its own go:generate directive.
I guess that makes sense for the average user running it directly,
but that doesn't apply to us, and we end up with duplicate directives.

Before:

	$ go generate -n
	bundle -o cmdgo_quoted.go -prefix cmdgoQuoted cmd/internal/quoted
	go run golang.org/x/tools/cmd/bundle@v0.1.9 -o cmdgo_quoted.go -prefix cmdgoQuoted cmd/internal/quoted

After:

	$ go generate -n
	go run golang.org/x/tools/cmd/bundle@v0.1.9 -o cmdgo_quoted.go -prefix cmdgoQuoted cmd/internal/quoted
	sed -i /go:generate/d cmdgo_quoted.go

While here, I made a typo in the last release notes, because of course.
I already edited that out in the GitHub release.
2 years ago
Daniel Martí 7608d524a5 CHANGELOG: v0.6.0 is happening one day late 2 years ago
Daniel Martí 13c0c930ee prepare changelog for v0.6.0
This time including links to issues, because I think that can be helpful
for those wanting to read more into each topic.

While here, to make the release links stand out more,
add the "v" prefix. This also matches the release names themselves.
2 years ago
Daniel Martí ab39ee804d fail if the current Go version is newer than what built garble
For instance, Go 1.18 added support for generics, so its compiler output
files changed format to accomodate for the new language feature.
If garble is built with Go 1.17 and then used to perform builds on Go
1.18, it will fail in a very confusing way, because garble's go/types
and go/importer packages will not know how to deal with that.

As already discussed in #269, require the version that built the garble
binary to be equal or newer. In that thread we discussed only comparing
the major version, so for example garble built on go1.18 could be used
on the toolchain go1.18.5. However, that could still fail in confusing
ways if a fix to go/types or go/importer happened in a point release.

While here, I noticed that we were still using Go 1.17 for some CI
checks. Fix that, except for staticcheck.

Fixes #269.
2 years ago
Daniel Martí 434de2e472 make early errors count towards code coverage
I recently added TODOs for bits of code we should cover in the tests.
I was looking at that again just now, and was puzzled;
we do indeed have test cases for many of them already.

We just weren't counting them towards code coverage due to a bug.
errJustExit works as expected, except that it calls os.Exit directly,
whereas testscript wants a non-zero return to run its "after" code.
Part of that code is what handles joining code coverage files.

The total code coverage jumps from 86.2% to 87.6%.
2 years ago
Daniel Martí 91ea246349 minor code tidying up
We don't need to nest the RemoveAll and MkdirAll calls for debugdir.

Fill the string for -toolexec= when we actually append it to goArgs.

Consistently use the objectString type alias.

Remove unnecessary parameter names in transformFuncs.

Unindent the code for switch variables by reversing the conditional.
2 years ago
Daniel Martí 807884f15a CI: bump gotip to March 14th and the final Go 1.18 2 years ago
Daniel Martí 1db12b7118 update Go module deps
Mainly for the x/tools update before the upcoming release,
and for Go 1.18 support.
2 years ago
Daniel Martí cd797e6e95 add a few TODOs with uncovered code that should not be
This will hopefully give new contributors a place to start.
Some of these, like verifying that the "help" commands work,
will be relatively simple additions to our test scripts.
2 years ago
Daniel Martí 8b55dd4bd2 work around a build cache regression in the previous commit
The added comment in main.go explains the situation in detail.
The added test is a minimization of the scenario, which failed:

        > cd mod1
        > garble -seed=${SEED1} build -v gopkg.in/garbletest.v2
        > cd ../mod2
        > garble -seed=${SEED1} build -v
        [stderr]
        test/main/mod2
        # test/main/mod2
        cannot load garble export file for gopkg.in/garbletest.v2: open […]/go-build/ed/[…]-garble-ZV[…]-d: no such file or directory

To work around the problem, we'll always add each package's
GarbleActionID to its build artifact, even when not using -seed.
This will get us the previous behavior with regards to the build cache,
meaning that we fix the recent regression.
The added variable doesn't make it to the final binary either.

While here, improve the cached file loading error's context,
and add an extra sanity check for duplicates on ListedPackages.
2 years ago
Daniel Martí c1c90fee13 make obfuscation fully deterministic with -seed
The default behavior of garble is to seed via the build inputs,
including the build IDs of the entire Go build of each package.
This works well as a default, and does give us determinism,
but it means that building for different platforms
will result in different obfuscation per platform.

Instead, when -seed is provided, don't use any other hash seed or salt.
This means that a particular Go name will be obfuscated the same way
as long as the seed, package path, and name itself remain constant.

In other words, when the user supplies a custom -seed,
we assume they know what they're doing in terms of storage and rotation.

Expand the README docs with more examples and detail.

Fixes #449.
2 years ago
Daniel Martí cf0351bdf5 remove ErrExist check on -debugdir's RemoveAll
This code initially used os.Stat, where it made sense to use
os.IsNotExist to catch whether the directory didn't exist.

However, we later transitioned to os.RemoveAll, which will never return
neither ErrExist nor ErrNotExist:

	If the path does not exist, RemoveAll returns nil (no error).

Simplify the code.
2 years ago
Daniel Martí bd3f950799 add a TODO about a possible cache bug with -literals and -ldflags
I spent some time trying to reproduce the bug but couldn't,
so for now, make a detailed note for it.
We can come back to it if we actually run into it in the future.

Fixes #492.
2 years ago
Daniel Martí ad87a0e2bc don't let -debug affect the build cache hashes
Noticed while debugging #492,
as adding -debug to a previously run command would unexpectedly
rebuild all packages in the build, as if -a was given.

While here, remove commented out testscript that were kept in error.
2 years ago
Daniel Martí 88a27d491b add support for -ldflags using quotes
In particular, using -ldflags with -

In particular, a command like:

	garble -literals build -ldflags='-X "main.foo=foo bar"'

would fail, because we would try to use "\"main" as the package name for
the -X qualified name, with the leading quote character.

This is because we used strings.Split(ldflags, " ").
Instead, use the same quoted.Split that cmd/go uses,
copied over thanks to x/tools/cmd/bundle and go:generate.

Updates #492.
2 years ago
Daniel Martí d8f6f308bd clarify how each "cannot obfuscate" map works
We used to record all objects in cannotObfuscateNames,
and then we'd add the exported ones to KnownCannotObfuscate.

Instead, teach recordAsNotObfuscated to store each object in either
knownCannotObfuscateUnexported or KnownCannotObfuscate, but not both.
The former isn't cached so it uses in-memory pointers as keys,
and the latter uses the cross-process objectStrings like before.

Functionally, this is all the same, but with the difference that the map
indexed by types.Object will not contain objects already recorded in
KnownCannotObfuscate, reducing the amount of duplicate memory use.

While here, give recordIgnore a less ambiguous name,
and remove the second parameter as it was always tf.pkg.Path().
This also means we can compare *types.Package pointers directly.

Finally, add more TODOs for further improvement ideas.
It does mean that we end up with more TODOs than before,
even though I'm fixing one, but I reckon that's a good thing.
Recording these ideas can give first-time contributors ways to help,
and it ensures I don't forget about ideas just in my head.
2 years ago
Daniel Martí a9a721e352 concentrate and simplify "to obfuscate" logic
Back in the day, we used to call toObfuscate anytime we needed to know
whether a package should be obfuscated.
More recently, we started computing via the ToObfuscate field,
which then gets shared with all sub-processes via sharedCache.

We still had two places that directly called toObfuscate.
Replace those with ToObfuscate, and inline toObfuscate into shared.go.

obfuscatedImportPath is also a potential footgun for main packages.
Some use cases always want the original "main" package name,
such as for use in the compiler's "-p main" flag,
while other cases want the obfuscated package import path,
such as the entries in importcfg files.

Since each of these call sites handles the edge case well,
obfuscatedImportPath now panics on main packages to avoid any misuse.

Finally, test that we never leak main package paths via ldflags.txt.
We never did, but it's good to make sure.

Overall, this avoids confusion and trims the size of main.go a bit.
2 years ago
Daniel Martí 34f85e3286 remove TODO for tf.pkg.Imports
I tried to fix the TODO, but ran into the problem described by the added
documentation - that some packages in the import graph are incomplete,
as go/types was clever and didn't fully load them.

While here, also make the panics a bit more descriptive,
which helped me debug what was going wrong after the attempted refactor.
2 years ago
Daniel Martí 5c1b2f17f8 CI: start testing on Go 1.18rc1, bump gotip to 1.19
Our upcoming release will focus on supporting 1.17.x and 1.18.x.
Since gotip is now merging features for the future 1.19.x,
let's add 1.18rc1 to the matrix and bump gotip.

This will make our CI jobs a bit slower for the time being,
but that's the price to pay to ensure we support both versions.
After the next garble version is released,
we can consider dropping support for 1.17.x.
2 years ago
Daniel Martí 70b1cb2fd8 CI: start enforcing vet and staticcheck
Fix a staticcheck warning about unused code,
as well as an unparam warning and a missing copyright header.

We also bump the action versions to their latest releases,
and drop unnecessary "name" fields for self-describing steps.

Note that we drop the "go env" commands, as setup-go does that now.

Finally, I did briefly try to add caching,
but then realised it didn't help us at all. Document why.
2 years ago
Daniel Martí 345ecda999 implement TODO to use a name variable
Slightly simplifies the main chunk of code.
While here, improve the wording for when Go isn't installed correctly.
2 years ago
Daniel Martí 955c24856c properly record when type aliases are embedded as fields
There are two scenarios when it comes to embedding fields.
The first is easy, and we always handled it well:

	type Named struct { Foo int }

	type T struct { Named }

In this scenario, T ends up with an embedded field named "Named",
and a promoted field named "Foo".

Then there's the form with a type alias:

	type Named struct { Foo int }

	type Alias = Named

	type T struct { Alias }

This case is different: T ends up with an embedded field named "Alias",
and a promoted field named "Foo".
Note how the field gets its name from the referenced type,
even if said type is just an alias to another type.

This poses two problems.
First, we must obfuscate the field T.Alias as the name "Alias",
and not as the name "Named" that the alias points to.
Second, we must be careful of cases where Named and Alias are declared
in different packages, as they will obfuscate the same name differently.

Both of those problems compounded in the reported issue.
The actual reason is that quic-go has a type alias in the form of:

	type ConnectionState = qtls.ConnectionState

In other words, the entire problem boils down to a type alias which
points to a named type in a different package, where both types share
the same name. For example:

	package parent

	import "parent/p1"

	type T struct { p1.SameName }

	[...]

	package p1

	import "parent/p2"

	type SameName = p2.SameName

	[...]

	package p2

	type SameName struct { Foo int }

This broke garble because we had a heuristic to detect when an embedded
field was a type alias:

	// Instead, detect such a "foreign alias embed".
	// If we embed a final named type,
	// but the field name does not match its name,
	// then it must have been done via an alias.
	// We dig out the alias's TypeName via locateForeignAlias.
	if named.Obj().Name() != node.Name {

As the reader can deduce, this heuristic would incorrectly assume that
the snippet above does not embed a type alias, when in fact it does.
When obfuscating the field T.SameName, which uses a type alias,
we would correctly obfuscate the name "SameName",
but we would incorrectly obfuscate it with the package p2, not p1.
This would then result in build errors.

To fix this problem for good, we need to get rid of the heuristic.
Instead, we now mimic what was done for KnownCannotObfuscate,
but for embedded fields which use type aliases.
KnownEmbeddedAliasFields is now filled for each package
and stored in the cache as part of cachedOutput.
We can then detect the "embedded alias" case reliably,
even when the field is declared in an imported package.

On the plus side, we get to remove locateForeignAlias.
We also add a couple of TODOs to record further improvements.
Finally, add a test.

Fixes #466.
2 years ago
Daniel Martí 73e91fd8c0 README: clarify that the first "garble build" may be slow
The section said that "garble build" should take about twice as long as
"go build", which is typically true, with the glaring exception that is
the first "garble build" after normal use of "go build".

This scenario will be rather common for first-time users,
and the difference will be most noticeable with large projects.
A large build may take about a second to rebuild with a warm cache,
but could take tens or even hundreds of seconds to rebuild from scratch.

See #479.
2 years ago
Daniel Martí 7994877a52 allocate slightly less in printFile
Reuse buffers, so they only grow once.
Join a fmt.Sprintf with a string addition.

	name      old time/op         new time/op         delta
	Build-16          9.39s ± 2%          9.28s ± 2%    ~     (p=0.548 n=5+5)

	name      old bin-B           new bin-B           delta
	Build-16          5.16M ± 0%          5.16M ± 0%    ~     (all equal)

	name      old cached-time/op  new cached-time/op  delta
	Build-16          324ms ± 6%          317ms ± 2%    ~     (p=0.421 n=5+5)

	name      old mallocs/op      new mallocs/op      delta
	Build-16          29.4M ± 0%          29.3M ± 0%  -0.25%  (p=0.008 n=5+5)

	name      old sys-time/op     new sys-time/op     delta
	Build-16          4.66s ± 2%          4.59s ± 2%    ~     (p=0.310 n=5+5)
2 years ago
Daniel Martí cffb5acd11 remove another allocation per hashed name
We use a buffer to turn a hash into a name via base64.
We can reuse that buffer to save one repeated allocation.
The returned value is still unique, as we convert to a string.

	name      old time/op         new time/op         delta
	Build-16          9.26s ± 3%          9.39s ± 2%    ~     (p=0.151 n=5+5)

	name      old bin-B           new bin-B           delta
	Build-16          5.16M ± 0%          5.16M ± 0%    ~     (all equal)

	name      old cached-time/op  new cached-time/op  delta
	Build-16          314ms ± 5%          324ms ± 6%    ~     (p=0.310 n=5+5)

	name      old mallocs/op      new mallocs/op      delta
	Build-16          29.8M ± 0%          29.4M ± 0%  -1.38%  (p=0.008 n=5+5)

	name      old sys-time/op     new sys-time/op     delta
	Build-16          4.70s ± 4%          4.66s ± 2%    ~     (p=0.548 n=5+5)
2 years ago