From e300c9596f35d4e99b444794432f33465b80ee08 Mon Sep 17 00:00:00 2001 From: Jin Xiao Date: Fri, 16 Oct 2026 06:59:26 +0800 Subject: [PATCH 1/2] perf: reduce JSON v2 snapshot and normalization overhead --- docs/CHECKLIST.md | 2 + docs/MODULE_ACCEPTANCE_SCOPED.json | 96 +++++++++------ docs/PERFORMANCE.md | 46 +++++++ docs/PERFORMANCE_JSON_V2_250.json | 28 +++++ internal/jsonvalue/snapshot.go | 90 ++++++++++++++ internal/jsonvalue/snapshot_benchmark_test.go | 34 ++++++ internal/jsonvalue/snapshot_test.go | 113 ++++++++++++++++++ jmespath/jmespath.go | 8 +- jmespath/snapshot_benchmark_test.go | 29 +++++ logger/correlation.go | 9 +- logger/correlation_test.go | 28 +++++ logger/encoding.go | 92 ++++++++++---- logger/normalization_performance_test.go | 71 +++++++++++ logger/snapshot_benchmark_test.go | 65 ++++++++++ validation/compiler.go | 4 +- validation/snapshot_benchmark_test.go | 23 ++++ validation/snapshot_decode_test.go | 68 +++++++++++ validation/validation.go | 8 +- 18 files changed, 736 insertions(+), 78 deletions(-) create mode 100644 docs/PERFORMANCE_JSON_V2_250.json create mode 100644 internal/jsonvalue/snapshot.go create mode 100644 internal/jsonvalue/snapshot_benchmark_test.go create mode 100644 internal/jsonvalue/snapshot_test.go create mode 100644 jmespath/snapshot_benchmark_test.go create mode 100644 logger/normalization_performance_test.go create mode 100644 logger/snapshot_benchmark_test.go create mode 100644 validation/snapshot_benchmark_test.go create mode 100644 validation/snapshot_decode_test.go diff --git a/docs/CHECKLIST.md b/docs/CHECKLIST.md index b48b3cb..e4cfb86 100644 --- a/docs/CHECKLIST.md +++ b/docs/CHECKLIST.md @@ -413,6 +413,8 @@ Race-detector execution is intentionally excluded because CGO must remain disabl ## Phased remaining work +- [x] PERF-JSON-V2-250-252: Reuse bounded private JSON-shaped snapshots for Logger correlation and JMESPath, retaining JSON v2 fallback for custom/raw/invalid/deep inputs and float64 conversion, full-input errors and ownership. Reduce Logger root/slice normalization allocations without changing callback keys, cycles, omission or atomic output; decode existing Validation snapshots directly with exact numbers and diagnostic order. Differential, ownership, concurrent-copy and existing module regressions pass. Four packaged modules and four independent consumers pass tests, vet, tidy and isolation with CGO disabled; licenses, scoped JSON API policy and composed static Linux amd64/arm64 Lambda builds pass. Record identical-source local before/after benchmarks in [performance evidence](PERFORMANCE_JSON_V2_250.json). This is scoped local acceptance, not all-module resource budgets or AWS acceptance; no API/dependency/version/release change. See issues #250, #251 and #252 (2026-10-16). + - [x] AUDIT-VALIDATION-189-LOCAL: Complete the [finite default factory matrix](VALIDATION_PLAN.md#finite-default-boundary-audit-189) with 43 actual public TypeScript/AJV default observations, twelve preserved strictTypes warnings and two original external registration failures. Reuse existing strict/setup/reference/graph/conditional/numeric/regex corpora; verify referenced enum snapshots, private nested diagnostics, channel-fenced once-only format cancellation and strict native JSON operational errors. No production defect was reproduced; typed registration errors and the absence of AJV logger side effects remain deliberate Go contracts. Packaged tests/vet/tidy/isolation and one independent consumer, scoped JSON v2 and documentation/discovery checks pass with CGO disabled. Exhaustive V-05/V-06/V-07, resources and deferred Draft 4/6 work remain separate (2026-10-13). - [x] AUDIT-JMESPATH-187-LOCAL: Complete the [finite grammar/number/serialization matrix](JMESPATH_PLAN.md#finite-boundary-audit-187) with 73 actual TypeScript observations and strict native input/custom-error checks. Fix numeric string conversion through Commons and replace delegated legacy serialization with JSON v2, retaining deterministic Go key order and typed errors. Also reproduce and fix #225 null multi-select evaluation using existing syntax nodes, preserving non-null false/zero/empty values and skipping null child callbacks. Packaged tests/vet/tidy/isolation and one independent consumer, scoped JSON policy and documentation/discovery checks pass with CGO disabled. Exhaustive J-08 and resource #200 gates remain open (2026-10-13). diff --git a/docs/MODULE_ACCEPTANCE_SCOPED.json b/docs/MODULE_ACCEPTANCE_SCOPED.json index aa717d7..3bb4bb6 100644 --- a/docs/MODULE_ACCEPTANCE_SCOPED.json +++ b/docs/MODULE_ACCEPTANCE_SCOPED.json @@ -4,12 +4,25 @@ "source": "local module proxy fixtures; not published versions", "completed": true, "modules": [ + { + "path": "github.com/rambow-cloud/powertools-lambda-go", + "directory": ".", + "public": true, + "version": "v1.2.0", + "archive_sha256": "5fe0865cac230095535a64921e85436ebdf56e0f74b340dc6f1aa1826b4930ff", + "tests": true, + "vet": true, + "tidy": true, + "consumer_build": true, + "external_modules": [], + "binary_bytes": 2613248 + }, { "path": "github.com/rambow-cloud/powertools-lambda-go/jmespath", "directory": "jmespath", "public": true, - "version": "v0.1.0", - "archive_sha256": "175484f2b375c7f22875d4e687ea002dc8dbfd3dfe4ab3612a8f716c1b802be0", + "version": "v1.2.0", + "archive_sha256": "6f9952a79b147fcdb1448769e5a8185b89535648f6dc6893b908320159a150ce", "tests": true, "vet": true, "tidy": true, @@ -26,61 +39,70 @@ "golang.org/x/tools", "gopkg.in/yaml.v3" ], - "binary_bytes": 3223040 + "binary_bytes": 3414016 }, { - "path": "github.com/rambow-cloud/powertools-lambda-go/parser", - "directory": "parser", + "path": "github.com/rambow-cloud/powertools-lambda-go/logger", + "directory": "logger", "public": true, - "version": "v0.1.0", - "archive_sha256": "750c955357a32433e4163bbefc03f2c1fa339b05490731b463c60c937386945a", + "version": "v1.2.0", + "archive_sha256": "7b42be1b2cb926180b3064321073cb6c9e1811f9161be6d9dc640442c76e321e", "tests": true, "vet": true, "tidy": true, "consumer_build": true, - "external_modules": [], - "binary_bytes": 4545024 + "external_modules": [ + "github.com/aws/aws-lambda-go", + "github.com/cespare/xxhash/v2", + "github.com/davecgh/go-spew", + "github.com/go-logr/logr", + "github.com/go-logr/stdr", + "github.com/google/go-cmp", + "github.com/pmezard/go-difflib", + "github.com/stretchr/testify", + "go.opentelemetry.io/auto/sdk", + "go.opentelemetry.io/otel", + "go.opentelemetry.io/otel/metric", + "go.opentelemetry.io/otel/trace", + "go.yaml.in/yaml/v3", + "gopkg.in/yaml.v3" + ], + "binary_bytes": 3955712 }, { - "path": "github.com/rambow-cloud/powertools-lambda-go/parameters", - "directory": "parameters", + "path": "github.com/rambow-cloud/powertools-lambda-go/validation", + "directory": "validation", "public": true, - "version": "v0.1.0", - "archive_sha256": "3adfda169361b436e67c5ae13e08caf4f3e244be587bb808e387358c959ed13c", + "version": "v1.2.0", + "archive_sha256": "82bf747382a904c32416fb4f62c855cc6f542ddc3a81146c497a60a4a310d349", "tests": true, "vet": true, "tidy": true, "consumer_build": true, "external_modules": [ - "github.com/aws/aws-sdk-go-v2", - "github.com/aws/aws-sdk-go-v2/config", - "github.com/aws/aws-sdk-go-v2/credentials", - "github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue", - "github.com/aws/aws-sdk-go-v2/feature/ec2/imds", - "github.com/aws/aws-sdk-go-v2/internal/configsources", - "github.com/aws/aws-sdk-go-v2/internal/endpoints/v2", - "github.com/aws/aws-sdk-go-v2/internal/v4a", - "github.com/aws/aws-sdk-go-v2/service/appconfigdata", - "github.com/aws/aws-sdk-go-v2/service/dynamodb", - "github.com/aws/aws-sdk-go-v2/service/dynamodbstreams", - "github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding", - "github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery", - "github.com/aws/aws-sdk-go-v2/service/internal/presigned-url", - "github.com/aws/aws-sdk-go-v2/service/secretsmanager", - "github.com/aws/aws-sdk-go-v2/service/signin", - "github.com/aws/aws-sdk-go-v2/service/ssm", - "github.com/aws/aws-sdk-go-v2/service/sso", - "github.com/aws/aws-sdk-go-v2/service/ssooidc", - "github.com/aws/aws-sdk-go-v2/service/sts", - "github.com/aws/smithy-go" + "github.com/davecgh/go-spew", + "github.com/dlclark/regexp2", + "github.com/dlclark/regexp2/v2", + "github.com/google/go-cmp", + "github.com/jmespath-community/go-jmespath", + "github.com/pmezard/go-difflib", + "github.com/santhosh-tekuri/jsonschema/v6", + "github.com/stretchr/testify", + "golang.org/x/exp", + "golang.org/x/mod", + "golang.org/x/sys", + "golang.org/x/text", + "golang.org/x/tools", + "gopkg.in/yaml.v3" ], - "binary_bytes": 3157504 + "binary_bytes": 6433792 } ], "scope": "Selected modules only; this report does not replace full-workspace acceptance", "selected_directories": [ + ".", "jmespath", - "parameters", - "parser" + "logger", + "validation" ] } diff --git a/docs/PERFORMANCE.md b/docs/PERFORMANCE.md index 759f4a3..acdf0ad 100644 --- a/docs/PERFORMANCE.md +++ b/docs/PERFORMANCE.md @@ -78,6 +78,52 @@ statistical regression budget or predict Linux Lambda latency. CloudWatch/stdout I/O, CPU quotas, GC under large-event workloads and cold starts require separate measurements. No AWS benchmark was run for this change. +## JSON v2 snapshot and normalization comparison (#250–#252) + +These changes keep JSON v2 and reduce conversions around it. An internal private +copy handles ordinary JSON-shaped inputs for correlation extraction and JMESPath. +Unsupported types, custom marshalers, raw JSON, invalid values, and deep/cyclic +inputs retain the JSON v2 round trip. The copy preserves float64 numbers and +default empty-container behavior; it does not bypass errors in unrelated fields. +Validation decodes an existing serialized snapshot directly with exact-number +options. Logger combines root preparation/normalization without callbacks and +avoids unused array-index strings while preserving the general callback path. + +The same new benchmark sources run against `d991b5af2d0e` and the candidate. +Measurements use Go 1.27.1, Windows amd64, `CGO_ENABLED=0`, `GOMAXPROCS=2`, and +one 500ms run per case. Large Logger/correlation/JMESPath cases use 2,048 records +with 512-byte messages. The large Validation case contains a 2 MiB string body; +it is a different workload from the earlier array-event experiments. + +| Operation | Before ns/op | After ns/op | Before B/op | After B/op | Before allocs/op | After allocs/op | +| --- | ---: | ---: | ---: | ---: | ---: | ---: | +| Invocation-bound INFO, 404 output bytes | 7,623 | 6,937 | 4,448 | 3,496 | 53 | 48 | +| Large event log, 1,141,832 output bytes | 5,078,787 | 4,314,234 | 3,963,179 | 3,590,002 | 26,406 | 22,569 | +| Large event correlation snapshot | 4,405,535 | 748,599 | 5,933,979 | 868,712 | 30,765 | 8,196 | +| Large event JMESPath lookup | 4,207,010 | 790,587 | 5,963,999 | 868,767 | 30,764 | 8,197 | +| Compiled Validation, small valid input | 5,044 | 4,184 | 2,321 | 2,241 | 55 | 52 | +| Compiled Validation, large string input | 4,763,843 | 2,955,717 | 6,512,209 | 4,484,167 | 39 | 33 | + +The identical direct-JSON invocation control retains 47 allocations, 2,773 bytes +and 404 output bytes. Filtered Logger calls retain zero allocations. Full raw +benchmark output and candidate production-source hashes are recorded in +[PERFORMANCE_JSON_V2_250.json](PERFORMANCE_JSON_V2_250.json). + +Run the maintained benchmarks from their owning modules with CGO disabled: + +```sh +CGO_ENABLED=0 GOMAXPROCS=2 go test -run '^$' -bench 'Benchmark(Logger|CorrelationSnapshot)' -benchmem -benchtime=500ms -count=1 . +CGO_ENABLED=0 GOMAXPROCS=2 go test -run '^$' -bench '^BenchmarkSearchSnapshot$' -benchmem -benchtime=500ms -count=1 . +CGO_ENABLED=0 GOMAXPROCS=2 go test -run '^$' -bench 'BenchmarkValidation(Compiled|LargeSnapshot)' -benchmem -benchtime=500ms -count=1 . +``` + +For a baseline checkout without the new benchmark files, copy only the matching +`snapshot_benchmark_test.go` from the candidate before measuring. The root +`internal/jsonvalue` benchmark also compares the private copy with a JSON v2 +round-trip control in the same binary. These are descriptive local measurements, +not a Lambda performance guarantee or permission to emit oversized cloud logs. +No AWS tests, new codec/parser, dependencies, versions or releases are included. + ## Measurement boundaries Construction and schema compilation happen outside the timed operations. Clocks diff --git a/docs/PERFORMANCE_JSON_V2_250.json b/docs/PERFORMANCE_JSON_V2_250.json new file mode 100644 index 0000000..66f674d --- /dev/null +++ b/docs/PERFORMANCE_JSON_V2_250.json @@ -0,0 +1,28 @@ +{ + "base_commit": "d991b5af2d0ef89cce228c8a6b716c9541714a4a", + "go": "go version go1.27.1 windows/amd64", + "os": "windows", + "architecture": "amd64", + "cgo_enabled": false, + "gomaxprocs": 2, + "benchtime": "500ms", + "count": 1, + "production_source_sha256": { + "jmespath/jmespath.go": "7919b52ae746f798704b4e7e18908fd70e6296430e17d860b2f4f7ba29ae1aa7", + "logger/correlation.go": "ee842ce1961216cac82550bbb4dfbb00150ff8e6abadc64ee8d2de7aeda6e2c0", + "logger/encoding.go": "f65c402c0ef32e88e5455eb87c2846a587fd676737839615f319ed5b19cea982", + "validation/compiler.go": "ca602c8b3dcf83c4629c7f0077dc16604dc0e8a26915da1e3c2aed6f192ba75b", + "validation/validation.go": "9ca8109ecb0b7306cdec61bddeed8686f2a34db973fcd9e83d05d9436c597be7", + "internal/jsonvalue/snapshot.go": "c092612d578ae7bb5ecc33983b0dced5ec2935f7a7d2b999fb643bfe91eb5cc5" + }, + "results": { + "logger-before": "goos: windows\ngoarch: amd64\npkg: github.com/rambow-cloud/powertools-lambda-go/logger\ncpu: 12th Gen Intel(R) Core(TM) i5-12500H\nBenchmarkLoggerInvocation/DirectJSON-2 \t 103356\t 5708 ns/op\t 404.0 output-B/op\t 2773 B/op\t 47 allocs/op\nBenchmarkLoggerInvocation/BoundINFO-2 \t 81723\t 7623 ns/op\t 404.0 output-B/op\t 4448 B/op\t 53 allocs/op\nBenchmarkLoggerInvocation/BindAndINFO-2 \t 75841\t 7969 ns/op\t 404.0 output-B/op\t 4737 B/op\t 54 allocs/op\nBenchmarkLoggerInvocation/WrappedINFO-2 \t 65239\t 9199 ns/op\t 404.0 output-B/op\t 6409 B/op\t 68 allocs/op\nBenchmarkLoggerInvocation/Filtered-2 \t31797344\t 17.98 ns/op\t 0 output-B/op\t 0 B/op\t 0 allocs/op\nBenchmarkLogger/Emitted-2 \t 102128\t 5927 ns/op\t 208.0 output-B/op\t 2752 B/op\t 34 allocs/op\nBenchmarkLogger/Filtered-2 \t33663799\t 16.94 ns/op\t 0 output-B/op\t 0 B/op\t 0 allocs/op\nBenchmarkLoggerLargeEvent-2 \t 116\t 5078787 ns/op\t 1141832 output-B/op\t 3963179 B/op\t 26406 allocs/op\nBenchmarkCorrelationSnapshot-2 \t 139\t 4405535 ns/op\t 5933979 B/op\t 30765 allocs/op\nPASS\nok \tgithub.com/rambow-cloud/powertools-lambda-go/logger\t6.637s\n", + "logger-after": "goos: windows\ngoarch: amd64\npkg: github.com/rambow-cloud/powertools-lambda-go/logger\ncpu: 12th Gen Intel(R) Core(TM) i5-12500H\nBenchmarkLoggerInvocation/DirectJSON-2 \t 109866\t 5506 ns/op\t 404.0 output-B/op\t 2773 B/op\t 47 allocs/op\nBenchmarkLoggerInvocation/BoundINFO-2 \t 88748\t 6937 ns/op\t 404.0 output-B/op\t 3496 B/op\t 48 allocs/op\nBenchmarkLoggerInvocation/BindAndINFO-2 \t 82346\t 7432 ns/op\t 404.0 output-B/op\t 3784 B/op\t 49 allocs/op\nBenchmarkLoggerInvocation/WrappedINFO-2 \t 73855\t 8732 ns/op\t 404.0 output-B/op\t 5457 B/op\t 63 allocs/op\nBenchmarkLoggerInvocation/Filtered-2 \t31664607\t 17.59 ns/op\t 0 output-B/op\t 0 B/op\t 0 allocs/op\nBenchmarkLogger/Emitted-2 \t 122216\t 4977 ns/op\t 208.0 output-B/op\t 2416 B/op\t 32 allocs/op\nBenchmarkLogger/Filtered-2 \t35763459\t 16.32 ns/op\t 0 output-B/op\t 0 B/op\t 0 allocs/op\nBenchmarkLoggerLargeEvent-2 \t 140\t 4314234 ns/op\t 1141832 output-B/op\t 3590002 B/op\t 22569 allocs/op\nBenchmarkCorrelationSnapshot-2 \t 801\t 748599 ns/op\t 868712 B/op\t 8196 allocs/op\nPASS\nok \tgithub.com/rambow-cloud/powertools-lambda-go/logger\t6.746s\n", + "jmespath-before": "goos: windows\ngoarch: amd64\npkg: github.com/rambow-cloud/powertools-lambda-go/jmespath\ncpu: 12th Gen Intel(R) Core(TM) i5-12500H\nBenchmarkSearchSnapshot-2 \t 140\t 4207010 ns/op\t 5963999 B/op\t 30764 allocs/op\nPASS\nok \tgithub.com/rambow-cloud/powertools-lambda-go/jmespath\t1.113s\n", + "jmespath-after": "goos: windows\ngoarch: amd64\npkg: github.com/rambow-cloud/powertools-lambda-go/jmespath\ncpu: 12th Gen Intel(R) Core(TM) i5-12500H\nBenchmarkSearchSnapshot-2 \t 808\t 790587 ns/op\t 868767 B/op\t 8197 allocs/op\nPASS\nok \tgithub.com/rambow-cloud/powertools-lambda-go/jmespath\t1.236s\n", + "validation-before": "goos: windows\ngoarch: amd64\npkg: github.com/rambow-cloud/powertools-lambda-go/validation\ncpu: 12th Gen Intel(R) Core(TM) i5-12500H\nBenchmarkValidationCompiled/Valid-2 \t 119641\t 5044 ns/op\t 2321 B/op\t 55 allocs/op\nBenchmarkValidationCompiled/Invalid-2 \t 46845\t 12502 ns/op\t 6775 B/op\t 158 allocs/op\nBenchmarkValidationLargeSnapshot-2 \t 126\t 4763843 ns/op\t 6512209 B/op\t 39 allocs/op\nPASS\nok \tgithub.com/rambow-cloud/powertools-lambda-go/validation\t2.912s\n", + "validation-after": "goos: windows\ngoarch: amd64\npkg: github.com/rambow-cloud/powertools-lambda-go/validation\ncpu: 12th Gen Intel(R) Core(TM) i5-12500H\nBenchmarkValidationCompiled/Valid-2 \t 158913\t 4184 ns/op\t 2241 B/op\t 52 allocs/op\nBenchmarkValidationCompiled/Invalid-2 \t 57051\t 11057 ns/op\t 6694 B/op\t 155 allocs/op\nBenchmarkValidationLargeSnapshot-2 \t 196\t 2955717 ns/op\t 4484167 B/op\t 33 allocs/op\nPASS\nok \tgithub.com/rambow-cloud/powertools-lambda-go/validation\t3.091s\n", + "snapshot": "goos: windows\ngoarch: amd64\npkg: github.com/rambow-cloud/powertools-lambda-go/internal/jsonvalue\ncpu: 12th Gen Intel(R) Core(TM) i5-12500H\nBenchmarkSnapshot/JSONv2RoundTrip-2 \t 139\t 4343293 ns/op\t 5455308 B/op\t 30758 allocs/op\nBenchmarkSnapshot/PrivateCopy-2 \t 753\t 763489 ns/op\t 868712 B/op\t 8196 allocs/op\nPASS\nok \tgithub.com/rambow-cloud/powertools-lambda-go/internal/jsonvalue\t1.812s\n" + }, + "limitations": "Local single-run descriptive measurements. Identical new benchmark sources in baseline and candidate. Large Logger/correlation/JMESPath fixture has 2048 records with 512-byte messages; Validation fixture has a 2 MiB string body. Counting sinks, no AWS or latency guarantee." +} diff --git a/internal/jsonvalue/snapshot.go b/internal/jsonvalue/snapshot.go new file mode 100644 index 0000000..81560c2 --- /dev/null +++ b/internal/jsonvalue/snapshot.go @@ -0,0 +1,90 @@ +package jsonvalue + +import ( + json "encoding/json/v2" + "math" + "unicode/utf8" +) + +// Snapshot returns the private, float64-number representation of a default +// JSON v2 marshal/unmarshal round trip. Ordinary JSON-shaped values can be +// copied without encoding; all other values retain the standard library path. +func Snapshot(value any) (any, error) { + if result, ok := snapshot(value, 0); ok { + return result, nil + } + raw, err := json.Marshal(value) + if err != nil { + return nil, err + } + var result any + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return result, nil +} + +func snapshot(input any, depth int) (any, bool) { + // Bound speculative copying without maintaining a second graph/cycle detector. + // Deep or cyclic inputs use JSON v2's own limits and diagnostics instead. + if depth > 64 { + return nil, false + } + switch value := input.(type) { + case nil, bool: + return value, true + case string: + return input, utf8.ValidString(value) + case float64: + return input, !math.IsNaN(value) && !math.IsInf(value, 0) + case int: + return float64(value), true + case int8: + return float64(value), true + case int16: + return float64(value), true + case int32: + return float64(value), true + case int64: + return float64(value), true + case uint: + return float64(value), true + case uint8: + return float64(value), true + case uint16: + return float64(value), true + case uint32: + return float64(value), true + case uint64: + return float64(value), true + case uintptr: + return float64(value), true + case map[string]any: + result := make(map[string]any, len(value)) + for key, item := range value { + if !utf8.ValidString(key) { + return nil, false + } + cloned, ok := snapshot(item, depth+1) + if !ok { + return nil, false + } + result[key] = cloned + } + return result, true + case []any: + result := make([]any, len(value)) + for i, item := range value { + cloned, ok := snapshot(item, depth+1) + if !ok { + return nil, false + } + result[i] = cloned + } + return result, true + default: + // Named types may implement marshalers. float32 also needs its JSON decimal + // representation before float64 conversion, rather than a direct Go cast. + return nil, false + } +} diff --git a/internal/jsonvalue/snapshot_benchmark_test.go b/internal/jsonvalue/snapshot_benchmark_test.go new file mode 100644 index 0000000..47db8a5 --- /dev/null +++ b/internal/jsonvalue/snapshot_benchmark_test.go @@ -0,0 +1,34 @@ +package jsonvalue_test + +import ( + "strings" + "testing" + + "github.com/rambow-cloud/powertools-lambda-go/internal/jsonvalue" +) + +var snapshotResult any + +func BenchmarkSnapshot(b *testing.B) { + records := make([]any, 2048) + for i := range records { + records[i] = map[string]any{"id": float64(i), "message": strings.Repeat("x", 512), "tags": []any{"a", "b", "c"}} + } + input := map[string]any{"records": records} + for _, name := range []string{"JSONv2RoundTrip", "PrivateCopy"} { + b.Run(name, func(b *testing.B) { + operation := roundTrip + if name == "PrivateCopy" { + operation = jsonvalue.Snapshot + } + b.ReportAllocs() + for b.Loop() { + value, err := operation(input) + if err != nil { + b.Fatal(err) + } + snapshotResult = value + } + }) + } +} diff --git a/internal/jsonvalue/snapshot_test.go b/internal/jsonvalue/snapshot_test.go new file mode 100644 index 0000000..2b0e2eb --- /dev/null +++ b/internal/jsonvalue/snapshot_test.go @@ -0,0 +1,113 @@ +package jsonvalue_test + +import ( + jsonv1 "encoding/json" + "encoding/json/jsontext" + json "encoding/json/v2" + "errors" + "math" + "reflect" + "sync" + "testing" + + "github.com/rambow-cloud/powertools-lambda-go/internal/jsonvalue" +) + +type customJSON struct { + calls *int + err error +} + +func (v customJSON) MarshalJSONTo(enc *jsontext.Encoder) error { + *v.calls++ + if v.err != nil { + return v.err + } + return enc.WriteValue(jsontext.Value(`{"value":12}`)) +} + +type namedString string + +func roundTrip(value any) (any, error) { + data, err := json.Marshal(value) + if err != nil { + return nil, err + } + var decoded any + err = json.Unmarshal(data, &decoded) + return decoded, err +} + +func TestSnapshotMatchesJSONv2(t *testing.T) { + cyclic := map[string]any{} + cyclic["cycle"] = cyclic + sliceCycle := make([]any, 1) + sliceCycle[0] = sliceCycle + var deep any = true + for range 80 { + deep = []any{deep} + } + values := []any{ + nil, true, "text", namedString("named"), math.Copysign(0, -1), float64(0.1), float32(0.1), + int(-123), int8(-128), int16(-32768), int32(math.MinInt32), int64(math.MinInt64), + uint(42), uint8(255), uint16(65535), uint32(math.MaxUint32), uint64(math.MaxUint64), uintptr(123), + jsonv1.Number("9007199254740993"), map[string]any(nil), []any(nil), []byte{1, 2}, + map[string]any{"nested": []any{map[string]any{"x": 1}, nil, []any(nil), map[string]any(nil)}}, + jsontext.Value(`{"large":9007199254740993}`), jsonv1.RawMessage(nil), + jsontext.Value(`{"x":1,"x":2}`), jsontext.Value(`{"id":"ok","unused":1e1000}`), + map[string]any{"id": "ok", "unused": math.NaN()}, math.Inf(1), + string([]byte{0xff}), map[string]any{string([]byte{0xff}): true}, + make(chan int), cyclic, sliceCycle, deep, + } + for i, input := range values { + want, wantErr := roundTrip(input) + got, gotErr := jsonvalue.Snapshot(input) + if (gotErr == nil) != (wantErr == nil) || (gotErr != nil && (reflect.TypeOf(gotErr) != reflect.TypeOf(wantErr) || gotErr.Error() != wantErr.Error())) { + t.Fatalf("case %d errors: got %v, want %v", i, gotErr, wantErr) + } + if gotErr == nil && !reflect.DeepEqual(got, want) { + t.Fatalf("case %d: got %#v, want %#v", i, got, want) + } + } + got, err := jsonvalue.Snapshot(math.Copysign(0, -1)) + if err != nil || !math.Signbit(got.(float64)) { + t.Fatal("lost negative zero") + } +} + +func TestSnapshotFallbackOnceAndOwnership(t *testing.T) { + calls := 0 + input := map[string]any{"custom": customJSON{calls: &calls}, "rows": []any{map[string]any{"v": 1}}} + snapshot, err := jsonvalue.Snapshot(input) + if err != nil || calls != 1 { + t.Fatal(err, calls) + } + snapshot.(map[string]any)["rows"].([]any)[0].(map[string]any)["v"] = false + if input["rows"].([]any)[0].(map[string]any)["v"] != 1 { + t.Fatal("aliased fallback input") + } + sentinel := errors.New("custom failure") + calls = 0 + _, err = jsonvalue.Snapshot(map[string]any{"custom": customJSON{calls: &calls, err: sentinel}}) + if calls != 1 || !errors.Is(err, sentinel) { + t.Fatal(calls, err) + } + shared := map[string]any{"rows": []any{map[string]any{"v": 1}}, "nil": []any(nil)} + var wg sync.WaitGroup + for range 24 { + wg.Add(1) + go func() { + defer wg.Done() + got, err := jsonvalue.Snapshot(shared) + if err != nil { + t.Error(err) + return + } + got.(map[string]any)["rows"].([]any)[0].(map[string]any)["v"] = false + }() + } + wg.Wait() + if shared["rows"].([]any)[0].(map[string]any)["v"] != 1 { + t.Fatal("aliased fast input") + } +} diff --git a/jmespath/jmespath.go b/jmespath/jmespath.go index 8d56a8c..e02ebb8 100644 --- a/jmespath/jmespath.go +++ b/jmespath/jmespath.go @@ -1,7 +1,6 @@ package jmespath import ( - json "encoding/json/v2" "errors" "fmt" "strings" @@ -9,6 +8,7 @@ import ( "github.com/jmespath-community/go-jmespath/pkg/interpreter" "github.com/jmespath-community/go-jmespath/pkg/parsing" "github.com/rambow-cloud/powertools-lambda-go/commons" + "github.com/rambow-cloud/powertools-lambda-go/internal/jsonvalue" ) // ErrorKind classifies failures without relying on engine error text. @@ -98,14 +98,10 @@ func Search(source string, data any, options ...Option) (any, error) { // Search accepts JSON-shaped values and typed Go events with JSON field tags. // Data is normalized to a private JSON snapshot; numbers use float64 as in JS. func (e *Expression) Search(data any) (any, error) { - encoded, err := json.Marshal(data) + input, err := jsonvalue.Snapshot(data) if err != nil { return nil, &Error{Kind: InvalidValue, Expression: e.source, Err: err} } - var input any - if err = json.Unmarshal(encoded, &input); err != nil { - return nil, &Error{Kind: InvalidValue, Expression: e.source, Err: err} - } result, err := interpreter.NewInterpreter(input, e.functions, nil).Execute(e.node, input) if err != nil { var queryError *Error diff --git a/jmespath/snapshot_benchmark_test.go b/jmespath/snapshot_benchmark_test.go new file mode 100644 index 0000000..d9e687d --- /dev/null +++ b/jmespath/snapshot_benchmark_test.go @@ -0,0 +1,29 @@ +package jmespath + +import ( + "strings" + "testing" +) + +var snapshotSearchResult any + +func BenchmarkSearchSnapshot(b *testing.B) { + records := make([]any, 2048) + for i := range records { + records[i] = map[string]any{"id": float64(i), "message": strings.Repeat("x", 512), "tags": []any{"a", "b", "c"}} + } + input := map[string]any{"records": records} + expression, err := Compile("records[0].id") + if err != nil { + b.Fatal(err) + } + b.ReportAllocs() + b.ResetTimer() + for b.Loop() { + value, err := expression.Search(input) + if err != nil { + b.Fatal(err) + } + snapshotSearchResult = value + } +} diff --git a/logger/correlation.go b/logger/correlation.go index 824d73f..ef49684 100644 --- a/logger/correlation.go +++ b/logger/correlation.go @@ -1,8 +1,9 @@ package logger import ( - json "encoding/json/v2" "fmt" + + "github.com/rambow-cloud/powertools-lambda-go/internal/jsonvalue" ) // CorrelationSource selects a built-in event location, not a JMESPath expression. @@ -38,14 +39,10 @@ func ExtractCorrelationID(source CorrelationSource, event any) (any, error) { default: return nil, fmt.Errorf("unknown correlation source %q", source) } - data, err := json.Marshal(event) + value, err := jsonvalue.Snapshot(event) if err != nil { return nil, err } - var value any - if err = json.Unmarshal(data, &value); err != nil { - return nil, err - } for _, key := range path { object, ok := value.(map[string]any) if !ok { diff --git a/logger/correlation_test.go b/logger/correlation_test.go index 5112cfa..7deee22 100644 --- a/logger/correlation_test.go +++ b/logger/correlation_test.go @@ -3,11 +3,39 @@ package logger import ( "bytes" "context" + "encoding/json/jsontext" + "math" "testing" "github.com/aws/aws-lambda-go/events" ) +func TestCorrelationSnapshotSemantics(t *testing.T) { + input := map[string]any{"id": map[string]any{"number": uint64(9007199254740993), "empty": []any(nil)}} + got, err := ExtractCorrelationID(EventBridge, input) + if err != nil { + t.Fatal(err) + } + object := got.(map[string]any) + if object["number"] != float64(9007199254740993) || object["empty"] == nil { + t.Fatal(object) + } + object["number"] = false + if input["id"].(map[string]any)["number"] != uint64(9007199254740993) { + t.Fatal("returned caller-owned object") + } + for _, event := range []any{ + map[string]any{"id": "valid", "unused": math.Inf(1)}, + map[string]any{"id": "valid", "unused": make(chan int)}, + jsontext.Value(`{"id":"valid","unused":1e1000}`), + jsontext.Value(`{"id":"valid","unused":{"x":1,"x":2}}`), + } { + if _, err := ExtractCorrelationID(EventBridge, event); err == nil { + t.Fatalf("accepted invalid unrelated member: %T", event) + } + } +} + func TestBuiltInCorrelationSources(t *testing.T) { for _, source := range []CorrelationSource{APIGatewayREST, APIGatewayHTTP, AppSyncAuthorizer, AppSyncResolver, ApplicationLoadBalancer, EventBridge, LambdaFunctionURL, S3ObjectLambda, VPCLattice} { event := map[string]any{"requestContext": map[string]any{"requestId": "request"}, "request": map[string]any{"headers": map[string]any{"x-amzn-trace-id": "request"}}, "headers": map[string]any{"x-amzn-trace-id": "request"}, "id": "request", "xAmzRequestId": "request"} diff --git a/logger/encoding.go b/logger/encoding.go index d6f1586..c3cbda4 100644 --- a/logger/encoding.go +++ b/logger/encoding.go @@ -206,6 +206,17 @@ func (n *normalizer) normalizeValue(key string, value any, depth int) (any, erro } return result, nil } + if values, ok := value.([]any); ok { + result := make([]any, len(values)) + for i, value := range values { + encoded, err := n.normalizeIndex(i, value, depth+1) + if err != nil { + return nil, err + } + result[i] = encoded + } + return result, nil + } switch v.Kind() { case reflect.Struct: return n.normalizeJSON(key, value, depth) @@ -236,7 +247,7 @@ func (n *normalizer) normalizeValue(key string, value any, depth int) (any, erro } result := make([]any, v.Len()) for i := range result { - encoded, err := n.normalize(fmt.Sprint(i), v.Index(i).Interface(), depth+1) + encoded, err := n.normalizeIndex(i, v.Index(i).Interface(), depth+1) if err != nil { return nil, err } @@ -247,6 +258,13 @@ func (n *normalizer) normalizeValue(key string, value any, depth int) (any, erro return value, nil } +func (n *normalizer) normalizeIndex(index int, value any, depth int) (any, error) { + if n.replacer == nil { + return n.normalizeValue("", value, depth) + } + return n.normalize(fmt.Sprint(index), value, depth) +} + // Apply descendant replacers to the JSON representation of structs/marshalers, // preserving field tags and exact numeric tokens without repeating the root callback. func (n *normalizer) normalizeJSON(key string, value any, depth int) (any, error) { @@ -325,35 +343,53 @@ func prepareForPrint(value any) any { func prepareFields(fields Fields) Fields { result := make(Fields, len(fields)) for key, value := range fields { - if value == nil { - continue + if keepPrintField(value) { + result[key] = value } - if text, ok := value.(string); ok { - if text != "" { - result[key] = value - } + } + return result +} + +func keepPrintField(value any) bool { + if value == nil { + return false + } + if text, ok := value.(string); ok { + return text != "" + } + // A named nil container can implement a marshaler; preserve its method. + _, jsonCustom := value.(json.Marshaler) + _, streamingCustom := value.(json.MarshalerTo) + _, textCustom := value.(encoding.TextMarshaler) + if !jsonCustom && !streamingCustom && !textCustom { + v := reflect.ValueOf(value) + switch v.Kind() { + case reflect.String: + return v.Len() != 0 + case reflect.Pointer, reflect.Map, reflect.Slice, reflect.Interface: + return !v.IsNil() + } + } + return true +} + +// normalizePrintFields fuses shallow root preparation with normalization when +// callbacks cannot observe that prepared root. Descendants still get private +// copies. Do not mark the original root active: the old shallow prepared copy +// was distinct from caller-owned cycles that point back to the original root. +func (n *normalizer) normalizePrintFields(fields Fields) (Fields, error) { + result := make(Fields, len(fields)) + for key, value := range fields { + if !keepPrintField(value) { continue } - // A named nil container can implement a marshaler; preserve its method. - _, jsonCustom := value.(json.Marshaler) - _, streamingCustom := value.(json.MarshalerTo) - _, textCustom := value.(encoding.TextMarshaler) - if !jsonCustom && !streamingCustom && !textCustom { - v := reflect.ValueOf(value) - switch v.Kind() { - case reflect.String: - if v.Len() == 0 { - continue - } - case reflect.Pointer, reflect.Map, reflect.Slice, reflect.Interface: - if v.IsNil() { - continue - } - } + encoded, err := n.normalizeValue(key, value, 1) + if err != nil { + return nil, err } - result[key] = value + result[key] = encoded } - return result + return result, nil } func encode(fields Fields, c config) ([]byte, error) { @@ -370,7 +406,11 @@ func (n *normalizer) encode(fields Fields, c config) ([]byte, error) { return nil, err } } - value, err = n.normalize("", prepareForPrint(value), 0) + if c.formatter == nil && c.replacer == nil { + value, err = n.normalizePrintFields(fields) + } else { + value, err = n.normalize("", prepareForPrint(value), 0) + } if err != nil { return nil, err } diff --git a/logger/normalization_performance_test.go b/logger/normalization_performance_test.go new file mode 100644 index 0000000..1fd69c9 --- /dev/null +++ b/logger/normalization_performance_test.go @@ -0,0 +1,71 @@ +package logger + +import ( + "encoding/json/jsontext" + json "encoding/json/v2" + "io" + "reflect" + "testing" +) + +type performanceEmptyString string + +func (performanceEmptyString) MarshalJSONTo(enc *jsontext.Encoder) error { + return enc.WriteToken(jsontext.String("custom")) +} + +func TestFusedRootMatchesPreparedRoot(t *testing.T) { + cyclic := Fields{} + cyclic["again"] = cyclic + inputs := []Fields{ + {"empty": "", "null": nil, "nil_map": map[string]any(nil), "nil_slice": []any(nil), "empty_map": Fields{}, "empty_slice": []any{}, "custom": performanceEmptyString("")}, + {"rows": []any{Fields{"name": "value"}, []string{"a", "b"}, []any(nil)}}, + cyclic, + } + for _, input := range inputs { + c := New(WithOutput(io.Discard)).cfg + data, err := encode(input, c) + if err != nil { + t.Fatal(err) + } + c.formatter = func(fields Fields) (any, error) { return fields, nil } + reference, err := encode(input, c) + if err != nil { + t.Fatal(err) + } + var got, want any + if err := json.Unmarshal(data, &got); err != nil { + t.Fatal(err) + } + if err := json.Unmarshal(reference, &want); err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, want) { + t.Fatalf("got %s, want %s", data, reference) + } + } + if _, ok := inputs[0]["empty"]; !ok { + t.Fatal("consumed caller fields") + } + rows := inputs[1]["rows"].([]any) + if rows[0].(Fields)["name"] != "value" { + t.Fatal("changed caller descendants") + } +} + +func TestSliceReplacerIndicesRemainObservable(t *testing.T) { + seen := map[string]int{} + c := New(WithOutput(io.Discard)).cfg + c.replacer = func(key string, value any) any { + if key == "0" || key == "1" { + seen[key]++ + } + return value + } + if _, err := encode(Fields{"plain": []any{"a", "b"}, "typed": []string{"c", "d"}}, c); err != nil { + t.Fatal(err) + } + if seen["0"] != 2 || seen["1"] != 2 { + t.Fatal(seen) + } +} diff --git a/logger/snapshot_benchmark_test.go b/logger/snapshot_benchmark_test.go new file mode 100644 index 0000000..1d1d289 --- /dev/null +++ b/logger/snapshot_benchmark_test.go @@ -0,0 +1,65 @@ +package logger + +import ( + json "encoding/json/v2" + "strings" + "testing" + "time" +) + +func largeSnapshotEvent() map[string]any { + records := make([]any, 2048) + for i := range records { + records[i] = map[string]any{"id": float64(i), "message": strings.Repeat("x", 512), "tags": []any{"a", "b", "c"}} + } + return map[string]any{"id": "correlation-id", "records": records} +} +func BenchmarkLoggerLargeEvent(b *testing.B) { + input := largeSnapshotEvent() + var out benchmarkWriter + l := New(WithOutput(&out), WithClock(func() time.Time { return time.Unix(0, 0).UTC() }), WithServiceName("benchmark")) + fields := Fields{"event": input} + b.ReportAllocs() + b.ResetTimer() + for b.Loop() { + if err := l.Info("event", fields); err != nil { + b.Fatal(err) + } + } + b.StopTimer() + b.ReportMetric(float64(out.bytes)/float64(b.N), "output-B/op") +} + +var correlationResult any + +func BenchmarkCorrelationSnapshot(b *testing.B) { + input := largeSnapshotEvent() + b.ReportAllocs() + b.ResetTimer() + for b.Loop() { + value, err := ExtractCorrelationID(EventBridge, input) + if err != nil { + b.Fatal(err) + } + correlationResult = value + } +} + +func TestLargeEventSnapshotRetainsCallerOwnership(t *testing.T) { + input := largeSnapshotEvent() + before, err := json.Marshal(input, json.Deterministic(true)) + if err != nil { + t.Fatal(err) + } + if _, err := ExtractCorrelationID(EventBridge, input); err != nil { + t.Fatal(err) + } + var out benchmarkWriter + if err := New(WithOutput(&out)).Info("event", Fields{"event": input}); err != nil { + t.Fatal(err) + } + after, err := json.Marshal(input, json.Deterministic(true)) + if err != nil || string(after) != string(before) { + t.Fatal("modified caller-owned event", err) + } +} diff --git a/validation/compiler.go b/validation/compiler.go index 9934fec..0acd351 100644 --- a/validation/compiler.go +++ b/validation/compiler.go @@ -108,7 +108,7 @@ func (jsonCompiler) Compile(ctx context.Context, input any, options CompileOptio if err != nil { return nil, err } - value, err := jsonValue(jsonv1.RawMessage(raw)) + value, err := decodeJSONValue(raw) if err != nil { return nil, err } @@ -167,7 +167,7 @@ func (v *jsonValidator) Validate(ctx context.Context, input any) (result []Issue return nil, err } snapshot := jsonv1.RawMessage(raw) - value, err := jsonValue(snapshot) + value, err := decodeJSONValue(raw) if err != nil { return nil, err } diff --git a/validation/snapshot_benchmark_test.go b/validation/snapshot_benchmark_test.go new file mode 100644 index 0000000..6f89ab0 --- /dev/null +++ b/validation/snapshot_benchmark_test.go @@ -0,0 +1,23 @@ +package validation + +import ( + "context" + "strings" + "testing" +) + +func BenchmarkValidationLargeSnapshot(b *testing.B) { + ctx := context.Background() + schema, err := Compile(ctx, map[string]any{"type": "object", "properties": map[string]any{"id": map[string]any{"type": "string"}}}, Options{}) + if err != nil { + b.Fatal(err) + } + input := map[string]any{"id": "valid", "body": strings.Repeat("x", 2*1024*1024)} + b.ReportAllocs() + b.ResetTimer() + for b.Loop() { + if _, err := schema.Validate(ctx, input); err != nil { + b.Fatal(err) + } + } +} diff --git a/validation/snapshot_decode_test.go b/validation/snapshot_decode_test.go new file mode 100644 index 0000000..bd51c4c --- /dev/null +++ b/validation/snapshot_decode_test.go @@ -0,0 +1,68 @@ +package validation + +import ( + "context" + jsonv1 "encoding/json" + "encoding/json/jsontext" + "errors" + "reflect" + "testing" +) + +type snapshotInput struct { + calls *int + fail error +} + +func (v snapshotInput) MarshalJSONTo(enc *jsontext.Encoder) error { + *v.calls++ + if v.fail != nil { + return v.fail + } + return enc.WriteValue(jsontext.Value(`{"large":9007199254740993,"exact":1.2300e+4}`)) +} + +func TestSerializedSnapshotPreservesExactNumbers(t *testing.T) { + raw := []byte(`{"large":9007199254740993,"exact":1.2300e+4,"nested":{"id":1}}`) + original := string(raw) + got, err := decodeJSONValue(raw) + if err != nil { + t.Fatal(err) + } + object := got.(map[string]any) + if object["large"] != jsonv1.Number("9007199254740993") || object["exact"] != jsonv1.Number("1.2300e+4") { + t.Fatal(object) + } + object["nested"].(map[string]any)["id"] = false + if string(raw) != original { + t.Fatal("changed diagnostic snapshot") + } + reference, err := jsonValue(jsontext.Value(raw)) + if err != nil { + t.Fatal(err) + } + delete(object, "nested") + delete(reference.(map[string]any), "nested") + if !reflect.DeepEqual(got, reference) { + t.Fatal(got, reference) + } +} + +func TestDefaultValidatorMarshalsInputOnce(t *testing.T) { + ctx := context.Background() + schema, err := Compile(ctx, map[string]any{"type": "object", "properties": map[string]any{ + "large": map[string]any{"const": jsonv1.Number("9007199254740993")}, + }}, Options{}) + if err != nil { + t.Fatal(err) + } + calls := 0 + if _, err := schema.Validate(ctx, snapshotInput{calls: &calls}); err != nil || calls != 1 { + t.Fatal(err, calls) + } + sentinel := errors.New("input failure") + calls = 0 + if _, err := schema.Validate(ctx, snapshotInput{calls: &calls, fail: sentinel}); !errors.Is(err, sentinel) || calls != 1 { + t.Fatal(err, calls) + } +} diff --git a/validation/validation.go b/validation/validation.go index 9f54863..2dd2f2c 100644 --- a/validation/validation.go +++ b/validation/validation.go @@ -124,8 +124,14 @@ func jsonValue(value any) (any, error) { if err != nil { return nil, err } + return decodeJSONValue(raw) +} + +// decodeJSONValue consumes an existing private JSON snapshot without encoding it +// a second time. Callers retain the bytes for diagnostics and object ordering. +func decodeJSONValue(raw []byte) (any, error) { var result any - if err = json.Unmarshal(raw, &result, jsonvalue.Numbers); err != nil { + if err := json.Unmarshal(raw, &result, jsonvalue.Numbers); err != nil { return nil, err } return result, nil From ff134c1eb620fb73bf2f435713232038af7a7de3 Mon Sep 17 00:00:00 2001 From: Jin Xiao Date: Fri, 16 Oct 2026 07:12:47 +0800 Subject: [PATCH 2/2] fix(modules): pin snapshot implementation for branch consumers --- docs/CHECKLIST.md | 2 +- docs/MODULE_ACCEPTANCE_SCOPED.json | 12 ++++++------ docs/PERFORMANCE.md | 6 +++++- examples/go.mod | 2 +- go.work | 2 ++ idempotency/cache/go.mod | 2 +- idempotency/go.mod | 2 +- integration/go.mod | 2 +- jmespath/go.mod | 2 +- logger/go.mod | 2 +- validation/go.mod | 2 +- 11 files changed, 21 insertions(+), 15 deletions(-) diff --git a/docs/CHECKLIST.md b/docs/CHECKLIST.md index e4cfb86..3629fd7 100644 --- a/docs/CHECKLIST.md +++ b/docs/CHECKLIST.md @@ -413,7 +413,7 @@ Race-detector execution is intentionally excluded because CGO must remain disabl ## Phased remaining work -- [x] PERF-JSON-V2-250-252: Reuse bounded private JSON-shaped snapshots for Logger correlation and JMESPath, retaining JSON v2 fallback for custom/raw/invalid/deep inputs and float64 conversion, full-input errors and ownership. Reduce Logger root/slice normalization allocations without changing callback keys, cycles, omission or atomic output; decode existing Validation snapshots directly with exact numbers and diagnostic order. Differential, ownership, concurrent-copy and existing module regressions pass. Four packaged modules and four independent consumers pass tests, vet, tidy and isolation with CGO disabled; licenses, scoped JSON API policy and composed static Linux amd64/arm64 Lambda builds pass. Record identical-source local before/after benchmarks in [performance evidence](PERFORMANCE_JSON_V2_250.json). This is scoped local acceptance, not all-module resource budgets or AWS acceptance; no API/dependency/version/release change. See issues #250, #251 and #252 (2026-10-16). +- [x] PERF-JSON-V2-250-252: Reuse bounded private JSON-shaped snapshots for Logger correlation and JMESPath, retaining JSON v2 fallback for custom/raw/invalid/deep inputs and float64 conversion, full-input errors and ownership. Reduce Logger root/slice normalization allocations without changing callback keys, cycles, omission or atomic output; decode existing Validation snapshots directly with exact numbers and diagnostic order. Differential, ownership, concurrent-copy and existing module regressions pass. Four packaged modules and four independent consumers pass tests, vet, tidy and isolation with CGO disabled; licenses, scoped JSON API policy and composed static Linux amd64/arm64 Lambda builds pass. Record identical-source local before/after benchmarks in [performance evidence](PERFORMANCE_JSON_V2_250.json). This is scoped local acceptance, not all-module resource budgets or AWS acceptance; no public API, external dependency or release-version change. Internal root requirements pin the snapshot implementation for independent branch consumers. See issues #250, #251 and #252 (2026-10-16). - [x] AUDIT-VALIDATION-189-LOCAL: Complete the [finite default factory matrix](VALIDATION_PLAN.md#finite-default-boundary-audit-189) with 43 actual public TypeScript/AJV default observations, twelve preserved strictTypes warnings and two original external registration failures. Reuse existing strict/setup/reference/graph/conditional/numeric/regex corpora; verify referenced enum snapshots, private nested diagnostics, channel-fenced once-only format cancellation and strict native JSON operational errors. No production defect was reproduced; typed registration errors and the absence of AJV logger side effects remain deliberate Go contracts. Packaged tests/vet/tidy/isolation and one independent consumer, scoped JSON v2 and documentation/discovery checks pass with CGO disabled. Exhaustive V-05/V-06/V-07, resources and deferred Draft 4/6 work remain separate (2026-10-13). diff --git a/docs/MODULE_ACCEPTANCE_SCOPED.json b/docs/MODULE_ACCEPTANCE_SCOPED.json index 3bb4bb6..dc1290f 100644 --- a/docs/MODULE_ACCEPTANCE_SCOPED.json +++ b/docs/MODULE_ACCEPTANCE_SCOPED.json @@ -9,7 +9,7 @@ "directory": ".", "public": true, "version": "v1.2.0", - "archive_sha256": "5fe0865cac230095535a64921e85436ebdf56e0f74b340dc6f1aa1826b4930ff", + "archive_sha256": "2265b8b026deb458121cd47f3b84e3b44099265c7ceb995fee97e94ef4ac69b0", "tests": true, "vet": true, "tidy": true, @@ -22,7 +22,7 @@ "directory": "jmespath", "public": true, "version": "v1.2.0", - "archive_sha256": "6f9952a79b147fcdb1448769e5a8185b89535648f6dc6893b908320159a150ce", + "archive_sha256": "787c9fdc193b837e0e1f97a18732156ec10337c46c78a9fc9a2226419c1ccd3f", "tests": true, "vet": true, "tidy": true, @@ -39,14 +39,14 @@ "golang.org/x/tools", "gopkg.in/yaml.v3" ], - "binary_bytes": 3414016 + "binary_bytes": 3414528 }, { "path": "github.com/rambow-cloud/powertools-lambda-go/logger", "directory": "logger", "public": true, "version": "v1.2.0", - "archive_sha256": "7b42be1b2cb926180b3064321073cb6c9e1811f9161be6d9dc640442c76e321e", + "archive_sha256": "c9d4151d2b2515991680fc953d5df18fad2d01a2c0d98eab48072913b1f6d302", "tests": true, "vet": true, "tidy": true, @@ -74,7 +74,7 @@ "directory": "validation", "public": true, "version": "v1.2.0", - "archive_sha256": "82bf747382a904c32416fb4f62c855cc6f542ddc3a81146c497a60a4a310d349", + "archive_sha256": "89b42d88162e26625cfa4b37d09f352688740b24148f1433fcd24a757f0b2335", "tests": true, "vet": true, "tidy": true, @@ -95,7 +95,7 @@ "golang.org/x/tools", "gopkg.in/yaml.v3" ], - "binary_bytes": 6433792 + "binary_bytes": 6434816 } ], "scope": "Selected modules only; this report does not replace full-workspace acceptance", diff --git a/docs/PERFORMANCE.md b/docs/PERFORMANCE.md index acdf0ad..b8d963d 100644 --- a/docs/PERFORMANCE.md +++ b/docs/PERFORMANCE.md @@ -122,7 +122,11 @@ For a baseline checkout without the new benchmark files, copy only the matching `internal/jsonvalue` benchmark also compares the private copy with a JSON v2 round-trip control in the same binary. These are descriptive local measurements, not a Lambda performance guarantee or permission to emit oversized cloud logs. -No AWS tests, new codec/parser, dependencies, versions or releases are included. +No AWS tests, new codec/parser, external dependencies or release versions are +included. Logger and JMESPath pin the internal root module to commit +`e300c9596f35` because the released root v1.2.0 lacks the new snapshot helper. +The corresponding Go pseudo-version and workspace mapping keep independent +branch consumers buildable without filesystem replacements in module files. ## Measurement boundaries diff --git a/examples/go.mod b/examples/go.mod index 285da90..95f4670 100644 --- a/examples/go.mod +++ b/examples/go.mod @@ -57,7 +57,7 @@ require ( github.com/google/uuid v1.6.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0 // indirect github.com/jmespath-community/go-jmespath v1.1.1 // indirect - github.com/rambow-cloud/powertools-lambda-go v1.2.0 // indirect + github.com/rambow-cloud/powertools-lambda-go v1.2.1-0.20261015225926-e300c9596f35 // indirect github.com/rambow-cloud/powertools-lambda-go/commons/awssdk v1.2.0 // indirect github.com/rambow-cloud/powertools-lambda-go/commons/regex v1.2.0 // indirect github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 // indirect diff --git a/go.work b/go.work index 5109445..6908622 100644 --- a/go.work +++ b/go.work @@ -36,6 +36,8 @@ use ( replace github.com/rambow-cloud/powertools-lambda-go v1.2.0 => . +replace github.com/rambow-cloud/powertools-lambda-go v1.2.1-0.20261015225926-e300c9596f35 => . + replace github.com/rambow-cloud/powertools-lambda-go/batch v1.2.0 => ./batch replace github.com/rambow-cloud/powertools-lambda-go/commons/awssdk v1.2.0 => ./commons/awssdk diff --git a/idempotency/cache/go.mod b/idempotency/cache/go.mod index 3cf86aa..e599e61 100644 --- a/idempotency/cache/go.mod +++ b/idempotency/cache/go.mod @@ -10,7 +10,7 @@ require ( require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/jmespath-community/go-jmespath v1.1.1 // indirect - github.com/rambow-cloud/powertools-lambda-go v1.2.0 // indirect + github.com/rambow-cloud/powertools-lambda-go v1.2.1-0.20261015225926-e300c9596f35 // indirect github.com/rambow-cloud/powertools-lambda-go/jmespath v1.2.0 // indirect go.uber.org/atomic v1.11.0 // indirect golang.org/x/exp v0.0.0-20230314191032-db074128a8ec // indirect diff --git a/idempotency/go.mod b/idempotency/go.mod index b5dc33e..95182fd 100644 --- a/idempotency/go.mod +++ b/idempotency/go.mod @@ -6,7 +6,7 @@ require ( github.com/aws/aws-sdk-go-v2 v1.47.0 github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.21.4 github.com/aws/aws-sdk-go-v2/service/dynamodb v1.68.0 - github.com/rambow-cloud/powertools-lambda-go v1.2.0 + github.com/rambow-cloud/powertools-lambda-go v1.2.1-0.20261015225926-e300c9596f35 github.com/rambow-cloud/powertools-lambda-go/commons/awssdk v1.2.0 github.com/rambow-cloud/powertools-lambda-go/jmespath v1.2.0 ) diff --git a/integration/go.mod b/integration/go.mod index a1e11eb..bbe56b2 100644 --- a/integration/go.mod +++ b/integration/go.mod @@ -11,7 +11,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/kms v1.51.1 github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.44.2 github.com/aws/aws-sdk-go-v2/service/ssm v1.78.0 - github.com/rambow-cloud/powertools-lambda-go v1.2.0 + github.com/rambow-cloud/powertools-lambda-go v1.2.1-0.20261015225926-e300c9596f35 github.com/rambow-cloud/powertools-lambda-go/batch v1.2.0 github.com/rambow-cloud/powertools-lambda-go/commons/metadata v1.2.0 github.com/rambow-cloud/powertools-lambda-go/commons/regex v1.2.0 diff --git a/jmespath/go.mod b/jmespath/go.mod index f7cd2d7..2bdfb53 100644 --- a/jmespath/go.mod +++ b/jmespath/go.mod @@ -4,7 +4,7 @@ go 1.27 require ( github.com/jmespath-community/go-jmespath v1.1.1 - github.com/rambow-cloud/powertools-lambda-go v1.2.0 + github.com/rambow-cloud/powertools-lambda-go v1.2.1-0.20261015225926-e300c9596f35 ) require golang.org/x/exp v0.0.0-20230314191032-db074128a8ec // indirect diff --git a/logger/go.mod b/logger/go.mod index 2aa6d06..93569b3 100644 --- a/logger/go.mod +++ b/logger/go.mod @@ -4,7 +4,7 @@ go 1.27 require ( github.com/aws/aws-lambda-go v1.55.0 - github.com/rambow-cloud/powertools-lambda-go v1.2.0 + github.com/rambow-cloud/powertools-lambda-go v1.2.1-0.20261015225926-e300c9596f35 go.opentelemetry.io/otel/trace v1.46.0 ) diff --git a/validation/go.mod b/validation/go.mod index 926b885..159b841 100644 --- a/validation/go.mod +++ b/validation/go.mod @@ -4,7 +4,7 @@ go 1.27 require ( github.com/dlclark/regexp2/v2 v2.8.0 - github.com/rambow-cloud/powertools-lambda-go v1.2.0 + github.com/rambow-cloud/powertools-lambda-go v1.2.1-0.20261015225926-e300c9596f35 github.com/rambow-cloud/powertools-lambda-go/commons/regex v1.2.0 github.com/rambow-cloud/powertools-lambda-go/jmespath v1.2.0 github.com/santhosh-tekuri/jsonschema/v6 v6.0.3