diff --git a/go.mod b/go.mod index 0000915af..169b29862 100644 --- a/go.mod +++ b/go.mod @@ -313,6 +313,7 @@ require ( github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect + github.com/neticdk/go-stdlib v1.0.0 // indirect github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d // indirect github.com/opencontainers/selinux v1.13.1 // indirect github.com/opentracing/opentracing-go v1.2.0 // indirect diff --git a/go.sum b/go.sum index 1fd6cf56d..2a41f59cf 100644 --- a/go.sum +++ b/go.sum @@ -542,6 +542,8 @@ github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/nberlee/go-netstat v0.1.2 h1:wgPV1YOUo+kDFypqiKgfxMtnSs1Wb42c7ahI4qyEUJc= github.com/nberlee/go-netstat v0.1.2/go.mod h1:GvDCRLsUKMRN1wULkt7tpnDmjSIE6YGf5zeVq+mBO64= +github.com/neticdk/go-stdlib v1.0.0 h1:9QCpoMpO5dBJGHJhumZrHzfJyvpVBd2Gc7ODJujfpXY= +github.com/neticdk/go-stdlib v1.0.0/go.mod h1:rch+DEB6VtR972ZPTY6A5OyLmCrp2YlXP0WGjuDDdcw= github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d h1:x3S6kxmy49zXVVyhcnrFqxvNVCBPb2KZ9hV2RBdS840= github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ= github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns= diff --git a/internal/app/machined/internal/server/v1alpha1/v1alpha1_server.go b/internal/app/machined/internal/server/v1alpha1/v1alpha1_server.go index bc28c72b8..8bbca1521 100644 --- a/internal/app/machined/internal/server/v1alpha1/v1alpha1_server.go +++ b/internal/app/machined/internal/server/v1alpha1/v1alpha1_server.go @@ -321,7 +321,7 @@ func (s *Server) ApplyConfiguration(ctx context.Context, in *machine.ApplyConfig } func generateDiff(r runtime.Runtime, provider config.Provider) (string, error) { - documentsDiff, err := configdiff.DiffToString(r.ConfigContainer(), provider) + documentsDiff, err := configdiff.DiffConfigs(r.ConfigContainer(), provider) if err != nil { return "", err } diff --git a/internal/app/machined/pkg/runtime/v1alpha1/v1alpha1_runtime.go b/internal/app/machined/pkg/runtime/v1alpha1/v1alpha1_runtime.go index 16605d91f..3195e6e01 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/v1alpha1_runtime.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/v1alpha1_runtime.go @@ -194,7 +194,7 @@ func (r *Runtime) CanApplyImmediate(cfg config.Provider) error { } if !reflect.DeepEqual(currentConfig, newConfig) { - diff, err := configdiff.DiffToString(container.NewV1Alpha1(currentConfig), container.NewV1Alpha1(newConfig)) + diff, err := configdiff.DiffConfigs(container.NewV1Alpha1(currentConfig), container.NewV1Alpha1(newConfig)) if err != nil { return fmt.Errorf("error calculating diff: %w", err) } diff --git a/pkg/machinery/config/configdiff/configdiff.go b/pkg/machinery/config/configdiff/configdiff.go index 4c0e9d937..c72752dd4 100644 --- a/pkg/machinery/config/configdiff/configdiff.go +++ b/pkg/machinery/config/configdiff/configdiff.go @@ -6,23 +6,15 @@ package configdiff import ( - "fmt" - "io" - "strings" - - "github.com/fatih/color" - "github.com/hexops/gotextdiff" - "github.com/hexops/gotextdiff/myers" - "github.com/hexops/gotextdiff/span" - "github.com/siderolabs/talos/pkg/machinery/config" "github.com/siderolabs/talos/pkg/machinery/config/encoder" + "github.com/siderolabs/talos/pkg/machinery/textdiff" ) -// Diff outputs (optionally) colorized diff between two machine configurations. +// DiffConfigs returns a string representation of the diff between two machine configurations. // // One of the resources might be nil. -func Diff(w io.Writer, oldCfg, newCfg config.Encoder) error { +func DiffConfigs(oldCfg, newCfg config.Encoder) (string, error) { var ( oldYaml, newYaml []byte err error @@ -31,99 +23,16 @@ func Diff(w io.Writer, oldCfg, newCfg config.Encoder) error { if oldCfg != nil { oldYaml, err = oldCfg.EncodeBytes(encoder.WithComments(encoder.CommentsDisabled)) if err != nil { - return err + return "", err } } if newCfg != nil { newYaml, err = newCfg.EncodeBytes(encoder.WithComments(encoder.CommentsDisabled)) if err != nil { - return err + return "", err } } - edits := myers.ComputeEdits(span.URIFromPath("a"), string(oldYaml), string(newYaml)) - diff := gotextdiff.ToUnified("a", "b", string(oldYaml), edits) - - outputDiff(w, diff, true) - - return nil -} - -// DiffToString returns a string representation of the diff between two machine configurations. -func DiffToString(oldCfg, newCfg config.Encoder) (string, error) { - var sb strings.Builder - - err := Diff(&sb, oldCfg, newCfg) - - return sb.String(), err -} - -//nolint:gocyclo -func outputDiff(w io.Writer, u gotextdiff.Unified, noColor bool) { - if len(u.Hunks) == 0 { - return - } - - bold := color.New(color.Bold) - cyan := color.New(color.FgCyan) - red := color.New(color.FgRed) - green := color.New(color.FgGreen) - - if noColor { - bold.DisableColor() - cyan.DisableColor() - red.DisableColor() - green.DisableColor() - } - - bold.Fprintf(w, "--- %s\n", u.From) //nolint:errcheck - bold.Fprintf(w, "+++ %s\n", u.To) //nolint:errcheck - - for _, hunk := range u.Hunks { - fromCount, toCount := 0, 0 - - for _, l := range hunk.Lines { - switch l.Kind { //nolint:exhaustive - case gotextdiff.Delete: - fromCount++ - case gotextdiff.Insert: - toCount++ - default: - fromCount++ - toCount++ - } - } - - cyan.Fprintf(w, "@@") //nolint:errcheck - - if fromCount > 1 { - cyan.Fprintf(w, " -%d,%d", hunk.FromLine, fromCount) //nolint:errcheck - } else { - cyan.Fprintf(w, " -%d", hunk.FromLine) //nolint:errcheck - } - - if toCount > 1 { - cyan.Fprintf(w, " +%d,%d", hunk.ToLine, toCount) //nolint:errcheck - } else { - cyan.Fprintf(w, " +%d", hunk.ToLine) //nolint:errcheck - } - - cyan.Fprintf(w, " @@\n") //nolint:errcheck - - for _, l := range hunk.Lines { - switch l.Kind { //nolint:exhaustive - case gotextdiff.Delete: - red.Fprintf(w, "-%s", l.Content) //nolint:errcheck - case gotextdiff.Insert: - green.Fprintf(w, "+%s", l.Content) //nolint:errcheck - default: - fmt.Fprintf(w, " %s", l.Content) - } - - if !strings.HasSuffix(l.Content, "\n") { - red.Fprintf(w, "\n\\ No newline at end of file\n") //nolint:errcheck - } - } - } + return textdiff.Diff(string(oldYaml), string(newYaml)) } diff --git a/pkg/machinery/config/configdiff/configdiff_test.go b/pkg/machinery/config/configdiff/configdiff_test.go index f8b1aa897..bf0e319ae 100644 --- a/pkg/machinery/config/configdiff/configdiff_test.go +++ b/pkg/machinery/config/configdiff/configdiff_test.go @@ -75,7 +75,7 @@ func TestDiffString(t *testing.T) { oldCfg := must.Value(container.New(test.oldCfg...))(t) newCfg := must.Value(container.New(test.newCfg...))(t) - got, err := configdiff.DiffToString(oldCfg, newCfg) + got, err := configdiff.DiffConfigs(oldCfg, newCfg) require.NoError(t, err) require.Equal(t, test.want, got) diff --git a/pkg/machinery/go.mod b/pkg/machinery/go.mod index c28214c11..efb44bcc4 100644 --- a/pkg/machinery/go.mod +++ b/pkg/machinery/go.mod @@ -12,13 +12,12 @@ require ( github.com/dustin/go-humanize v1.0.1 github.com/emicklei/dot v1.11.0 github.com/evanphx/json-patch v5.9.11+incompatible - github.com/fatih/color v1.18.0 github.com/ghodss/yaml v1.0.0 github.com/google/cel-go v0.27.0 github.com/hashicorp/go-multierror v1.1.1 - github.com/hexops/gotextdiff v1.0.3 github.com/jsimonetti/rtnetlink/v2 v2.2.0 github.com/mdlayher/ethtool v0.5.0 + github.com/neticdk/go-stdlib v1.0.0 github.com/opencontainers/runtime-spec v1.3.0 github.com/planetscale/vtprotobuf v0.6.1-0.20250313105119-ba97887b0a25 github.com/ryanuber/go-glob v1.0.0 @@ -55,8 +54,6 @@ require ( github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/josharian/native v1.1.0 // indirect - github.com/mattn/go-colorable v0.1.14 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect github.com/mdlayher/genetlink v1.3.2 // indirect github.com/mdlayher/netlink v1.8.0 // indirect github.com/mdlayher/socket v0.5.1 // indirect diff --git a/pkg/machinery/go.sum b/pkg/machinery/go.sum index d2f8b7c96..426f27b2e 100644 --- a/pkg/machinery/go.sum +++ b/pkg/machinery/go.sum @@ -34,8 +34,6 @@ github.com/emicklei/dot v1.11.0 h1:zsrhCuFHAJge/aZIC4N4LdHy5tqYu4tWEaUzIwdYj4Y= github.com/emicklei/dot v1.11.0/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s= github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8= github.com/evanphx/json-patch v5.9.11+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= -github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= -github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/gertd/go-pluralize v0.2.1 h1:M3uASbVjMnTsPb0PNqg+E/24Vwigyo/tvyMTtAlLgiA= github.com/gertd/go-pluralize v0.2.1/go.mod h1:rbYaKDbsXxmRfr8uygAEKhOWsjyrrqrkHVpZvoOp8zk= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= @@ -63,8 +61,6 @@ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= -github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/jsimonetti/rtnetlink/v2 v2.2.0 h1:/KfZ310gOAFrXXol5VwnFEt+ucldD/0dsSRZwpHCP9w= @@ -73,16 +69,14 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= -github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw= github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o= github.com/mdlayher/netlink v1.8.0 h1:e7XNIYJKD7hUct3Px04RuIGJbBxy1/c4nX7D5YyvvlM= github.com/mdlayher/netlink v1.8.0/go.mod h1:UhgKXUlDQhzb09DrCl2GuRNEglHmhYoWAHid9HK3594= github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos= github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ= +github.com/neticdk/go-stdlib v1.0.0 h1:9QCpoMpO5dBJGHJhumZrHzfJyvpVBd2Gc7ODJujfpXY= +github.com/neticdk/go-stdlib v1.0.0/go.mod h1:rch+DEB6VtR972ZPTY6A5OyLmCrp2YlXP0WGjuDDdcw= github.com/onsi/ginkgo/v2 v2.20.1 h1:YlVIbqct+ZmnEph770q9Q7NVAz4wwIiVNahee6JyUzo= github.com/onsi/ginkgo/v2 v2.20.1/go.mod h1:lG9ey2Z29hR41WMVthyJBGUBcBhGOtoPF2VFMvBXFCI= github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= @@ -178,7 +172,6 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= diff --git a/pkg/machinery/textdiff/textdiff.go b/pkg/machinery/textdiff/textdiff.go new file mode 100644 index 000000000..cfe06a13a --- /dev/null +++ b/pkg/machinery/textdiff/textdiff.go @@ -0,0 +1,77 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +// Package textdiff provides a way to compare two text blobs. +package textdiff + +import ( + "fmt" + "strings" + + "github.com/neticdk/go-stdlib/diff/myers" +) + +// MaxLines is the maximum number of lines that the diff function will process before giving up and returning a message instead. +const MaxLines = 75_000 + +// Diff is a function that computes unified diff between two strings. +// +// The diff is limited to MaxLines lines, and if the diff is larger than that, a message is returned instead of the actual diff. +// This is to prevent the function from consuming too much memory or CPU time when comparing large text blobs. +func Diff(a, b string) (string, error) { + if a == b { + return "", nil + } + + prevLines := strings.Count(a, "\n") + newLines := strings.Count(b, "\n") + + if prevLines+newLines > MaxLines { + return fmt.Sprintf("@@ -%d,%d +%d,%d @@ diff too large to display\n", 1, prevLines, 1, newLines), nil + } + + differ := myers.NewCustomDiffer( + myers.WithUnifiedFormatter(), + myers.WithLinearSpace(true), + // Disable the library's standard-Myers and LCS fallback paths: + // - Standard Myers (< smallInputThreshold) is O((N+M)²) when inputs are asymmetric. + // - LCS (> largeInputThreshold) is O(N*M) for the DP table. + // By setting these to 0 and MaxLines respectively, only Hirschberg's + // O(N+M) linear-space algorithm runs. Our MaxLines guard above ensures + // inputs never exceed largeInputThreshold. + myers.WithSmallInputThreshold(0), + myers.WithLargeInputThreshold(MaxLines), + ) + + return differ.Diff(a, b) +} + +// DiffWithCustomPaths is almost same as Diff, but allows to specify custom paths for the diff header. +func DiffWithCustomPaths(a, b, aPath, bPath string) (string, error) { + diff, err := Diff(a, b) + if err != nil { + return "", err + } + + if diff == "" { + return "", nil + } + + // patch the diff header to include the manifest path + diff, ok := strings.CutPrefix(diff, "--- a\n+++ b\n") + if !ok { + return "", fmt.Errorf("unexpected diff format") + } + + var sb strings.Builder + + sb.WriteString("--- ") + sb.WriteString(aPath) + sb.WriteString("\n+++ ") + sb.WriteString(bPath) + sb.WriteString("\n") + sb.WriteString(diff) + + return sb.String(), nil +} diff --git a/pkg/machinery/textdiff/textdiff_test.go b/pkg/machinery/textdiff/textdiff_test.go new file mode 100644 index 000000000..6979fa693 --- /dev/null +++ b/pkg/machinery/textdiff/textdiff_test.go @@ -0,0 +1,193 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package textdiff_test + +import ( + "fmt" + "math/rand/v2" + "runtime" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/siderolabs/talos/pkg/machinery/textdiff" +) + +func TestDiff(t *testing.T) { + t.Parallel() + + for _, test := range []struct { + name string + a, b string + + expected string + }{ + { + name: "empty", + a: "", + b: "", + expected: "", + }, + { + name: "identical", + a: "line 1\nline 2\nline 3\n", + b: "line 1\nline 2\nline 3\n", + + expected: "", + }, + { + name: "completely different", + a: "line 1\nline 2\nline 3\n", + b: "line A\nline B\nline C\n", + + expected: `--- a ++++ b +@@ -1,3 +1,3 @@ +-line 1 +-line 2 +-line 3 ++line A ++line B ++line C +`, + }, + { + name: "inserted line", + a: "line 1\nline 3\n", + b: "line 1\nline 2\nline 3\n", + + expected: `--- a ++++ b +@@ -1,2 +1,3 @@ + line 1 ++line 2 + line 3 +`, + }, + } { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + diff, err := textdiff.Diff(test.a, test.b) + require.NoError(t, err) + assert.Equal(t, test.expected, diff) + }) + } +} + +func TestDiffWithCustomPaths(t *testing.T) { + t.Parallel() + + diff, err := textdiff.DiffWithCustomPaths("line 1\nline 3\n", "line 1\nline 2\nline 3\n", "fileA.txt", "fileB.txt") + require.NoError(t, err) + assert.Equal(t, `--- fileA.txt ++++ fileB.txt +@@ -1,2 +1,3 @@ + line 1 ++line 2 + line 3 +`, diff) +} + +func genRandomLines(n int) string { + var sb strings.Builder + + for range n { + fmt.Fprintf(&sb, "line %d\n", rand.Int()) + } + + return sb.String() +} + +func TestDiffMemoryBudge(t *testing.T) { + // not parallel, we need to measure memory allocations + linesA := genRandomLines(1000) + linesB := genRandomLines(20000) + linesC := genRandomLines(1000) + + for _, test := range []struct { + name string + a, b string + + memoryBudget uint64 + }{ + { + name: "empty", + a: "", + b: "", + + memoryBudget: 1024, // 1KB + }, + { + name: "large", + a: linesA + linesB, + b: linesB + linesC, + + memoryBudget: 3 * 1024 * 1024, // 3MB + }, + { + name: "large 2", + a: linesA + linesB, + b: linesA + linesB + linesC, + + memoryBudget: 2 * 1024 * 1024, // 2MB + }, + { + name: "completely different", + a: linesA, + b: linesC, + + memoryBudget: 512 * 1024, // 512KB + }, + { + name: "large to empty", + + a: linesA + linesB + linesC, + b: "", + + memoryBudget: 6 * 1024 * 1024, // 6MB + }, + { + name: "empty to large", + + a: "", + b: linesA + linesB + linesC, + + memoryBudget: 6 * 1024 * 1024, // 6MB + }, + } { + t.Run(test.name, func(t *testing.T) { + differ := textdiff.Diff + + allocs := MemoryAllocated(func() { + _, err := differ(test.a, test.b) + require.NoError(t, err) + }) + + require.LessOrEqual(t, allocs, test.memoryBudget, "memory allocations exceeded the budget") + }) + } +} + +func MemoryAllocated(f func()) uint64 { + defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(1)) + + runtime.GC() + + // Measure the starting statistics + var memstats runtime.MemStats + runtime.ReadMemStats(&memstats) + allocs := 0 - memstats.TotalAlloc + + f() + + // Read the final statistics + runtime.ReadMemStats(&memstats) + allocs += memstats.TotalAlloc + + return allocs +}