perf(core): audit optimizations for reflection, eventloop, and memory handling#154
perf(core): audit optimizations for reflection, eventloop, and memory handling#154repyh wants to merge 1 commit into
Conversation
… handling
- **Reflection Optimization**: Reduced `interface{}` boxing in `bindMap` by replacing `v.MapKeys()` with `v.MapRange()` and avoiding generic `fmt.Sprint` for primitive integer keys.
- **ArrayBuffer Boxing**: Modified `bindSlice` to skip interface element-wrapping for `[]byte`, mapping directly to a JS `Uint8Array`. Handled unaddressable array copying explicitly.
- **Memory Safety**: `ToArrayBuffer` now explicitly copies slices instead of directly wrapping memory, solving a potentially lethal shared-memory vulnerability between JS and Go.
- **EventLoop Deadlock Fix**: Restored `sync.Once` logic inside `CreatePromise` to guarantee `wg.Done()` is only dispatched once, averting `negative WaitGroup counter` panics on double resolve/reject sequences.
- Added comprehensive documentation and explicit fallback logic for globals like `Uint8Array` in `MapSharedBuffer`.
Co-authored-by: repyh <63894915+repyh@users.noreply.github.com>
|
👋 Jules, reporting for duty! I'm here to lend a hand with this pull request. When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down. I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job! For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with New to Jules? Learn more at jules.google/docs. For security, I will only act on instructions from the user who triggered this task. |
📝 WalkthroughWalkthroughThis PR hardens the Go-JavaScript bridge by adding memory-safe ArrayBuffer copying, providing TypedArray fallback logic, optimizing value binding via strconv, and making promise settlement idempotent with sync.Once guards. Four files are modified across three related safety improvements. ChangesGo-JS Bridge Memory Safety and Promise Handling
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
bridge/core/arraybuffer.go (1)
20-51:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winDoc comment becomes inaccurate in the fallback path.
The function-level doc (Lines 24–26) promises the global is "accessible as a Uint8Array in JavaScript", but the new fallback at Lines 39–43 / 46–49 exposes a raw
ArrayBufferwhenUint8Arrayis unavailable or construction fails. Worth a one-line clarification so callers don't assumeglobalThis[name].byteLength-style typed-array semantics in degraded runtimes.📝 Proposed doc tweak
// The buffer is registered as a global variable with the given name, accessible -// as a Uint8Array in JavaScript. This is commonly used for inter-worker -// communication and zero-copy data sharing. +// as a Uint8Array in JavaScript when the Uint8Array constructor is available; +// otherwise it falls back to exposing the raw ArrayBuffer. This is commonly +// used for inter-worker communication and zero-copy data sharing.Side note: in that fallback path, downstream JS code has no way to read/write individual bytes without a typed-array constructor, so the "shared memory" contract is effectively reduced to handing JS an opaque buffer. That's fine as a crash-avoidance measure, but if a stricter behavior (e.g., error / log) is preferred, consider surfacing the degraded mode.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@bridge/core/arraybuffer.go` around lines 20 - 51, The function doc for MapSharedBuffer is inaccurate in degraded runtimes: update the comment to state that while normally the global is exposed as a Uint8Array, if the runtime lacks Uint8Array (checked via vm.Get("Uint8Array")) or vm.New(ctor.ToObject(vm), ...) fails the function exposes the raw ArrayBuffer created by vm.NewArrayBuffer; explicitly note that in this fallback the global is an ArrayBuffer (opaque to per-byte reads/writes without a typed-array) so callers should not assume Uint8Array semantics.
🧹 Nitpick comments (1)
bridge/core/reflection.go (1)
340-355: ⚡ Quick winVerify downstream impact:
[]bytenow binds asUint8Arrayinstead ofArray.This is a deliberate semantic change: downstream JS code expecting array methods may break:
Array.isArray(v)— returnsfalseforUint8Array.JSON.stringify(v)— produces{"0":1,"1":2,...}instead of[1,2,...]..map()/.filter()— return anotherUint8Array, notArray.Confirm that existing tests and any JS bindings handling bound Go structs with
[]bytefields still pass. (Current test coverage doesn't exercise this path explicitly.)Optional: simplify unaddressable-array branch with
reflect.Copy.The manual element-by-element loop at lines 347–352 can be replaced with
reflect.Copy, which is idiomatic and avoids per-elementUint()conversion:♻️ Optional cleanup using reflect.Copy
var data []byte if v.CanAddr() || v.Kind() == reflect.Slice { data = v.Bytes() } else { - // Unaddressable array, we must copy it element by element or create a slice copy - l := v.Len() - data = make([]byte, l) - for i := 0; i < l; i++ { - data[i] = byte(v.Index(i).Uint()) - } + // Unaddressable array: copy into an addressable []byte. + data = make([]byte, v.Len()) + reflect.Copy(reflect.ValueOf(data), v) } return ToUint8Array(vm, data), nil🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@bridge/core/reflection.go` around lines 340 - 355, The change in bindSlice causes []byte to bind as a Uint8Array (via ToUint8Array) which is a semantic change from Array and can break downstream JS (Array.isArray, JSON.stringify, Array methods); either revert to returning a JS Array for []byte or add a clear compatibility path/flag and update tests to assert expected behavior for Array.isArray(JSON.stringify(...), map/filter results) when binding []byte (check bindSlice and ToUint8Array usages). Also, replace the manual element-copy loop in bindSlice's unaddressable-array branch with reflect.Copy by creating a new slice via reflect.MakeSlice, reflect.Copy into it, then convert to []byte to pass to ToUint8Array for correctness and simplicity.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@eventloop/eventloop_bench_test.go`:
- Around line 11-20: The benchmark must disable the event loop's auto-stop and
fully drain pending jobs in cleanup: create the loop with autoStop disabled
(e.g. pass the WithAutoStop(false) option to NewEventLoop or call
el.SetAutoStop(false) before Start()), then start it (el.Start()), run the
CreatePromise/resolve loop, and on teardown call el.Stop() and then drain/wait
for the loop to process remaining jobs (e.g. el.Drain() or el.WaitForEmpty()) so
resolve() never writes into an unserviced jobQueue; update references around
NewEventLoop, el.Start, CreatePromise, resolve, el.Stop and the loop drain API
in the file.
---
Outside diff comments:
In `@bridge/core/arraybuffer.go`:
- Around line 20-51: The function doc for MapSharedBuffer is inaccurate in
degraded runtimes: update the comment to state that while normally the global is
exposed as a Uint8Array, if the runtime lacks Uint8Array (checked via
vm.Get("Uint8Array")) or vm.New(ctor.ToObject(vm), ...) fails the function
exposes the raw ArrayBuffer created by vm.NewArrayBuffer; explicitly note that
in this fallback the global is an ArrayBuffer (opaque to per-byte reads/writes
without a typed-array) so callers should not assume Uint8Array semantics.
---
Nitpick comments:
In `@bridge/core/reflection.go`:
- Around line 340-355: The change in bindSlice causes []byte to bind as a
Uint8Array (via ToUint8Array) which is a semantic change from Array and can
break downstream JS (Array.isArray, JSON.stringify, Array methods); either
revert to returning a JS Array for []byte or add a clear compatibility path/flag
and update tests to assert expected behavior for
Array.isArray(JSON.stringify(...), map/filter results) when binding []byte
(check bindSlice and ToUint8Array usages). Also, replace the manual element-copy
loop in bindSlice's unaddressable-array branch with reflect.Copy by creating a
new slice via reflect.MakeSlice, reflect.Copy into it, then convert to []byte to
pass to ToUint8Array for correctness and simplicity.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 59d3e334-0585-4bc7-8bce-fb245dcf7210
📒 Files selected for processing (4)
bridge/core/arraybuffer.gobridge/core/reflection.goeventloop/eventloop.goeventloop/eventloop_bench_test.go
| el := eventloop.NewEventLoop(vm) | ||
| go el.Start() | ||
| b.ResetTimer() | ||
| for i := 0; i < b.N; i++ { | ||
| _, resolve, _ := el.CreatePromise() | ||
| resolve(nil) | ||
| resolve(nil) // should not panic/deadlock now | ||
| } | ||
| el.Stop() | ||
| } |
There was a problem hiding this comment.
Disable auto-stop and drain the loop in cleanup.
With the default autoStop=true, go el.Start() can stop before the first CreatePromise() increments the wait group. After that, resolve() pushes into an unserviced jobQueue, so this benchmark can hang once the buffer fills and may exit before the last settlements run.
Proposed fix
import (
+ "context"
"testing"
- "github.com/repyh/typego/eventloop"
"github.com/grafana/sobek"
+ "github.com/repyh/typego/eventloop"
+ "time"
)
func BenchmarkCreatePromise(b *testing.B) {
vm := sobek.New()
el := eventloop.NewEventLoop(vm)
+ el.SetAutoStop(false)
go el.Start()
+ b.Cleanup(func() {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+ defer cancel()
+ _ = el.Shutdown(ctx)
+ })
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, resolve, _ := el.CreatePromise()
resolve(nil)
resolve(nil) // should not panic/deadlock now
}
- el.Stop()
}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@eventloop/eventloop_bench_test.go` around lines 11 - 20, The benchmark must
disable the event loop's auto-stop and fully drain pending jobs in cleanup:
create the loop with autoStop disabled (e.g. pass the WithAutoStop(false) option
to NewEventLoop or call el.SetAutoStop(false) before Start()), then start it
(el.Start()), run the CreatePromise/resolve loop, and on teardown call el.Stop()
and then drain/wait for the loop to process remaining jobs (e.g. el.Drain() or
el.WaitForEmpty()) so resolve() never writes into an unserviced jobQueue; update
references around NewEventLoop, el.Start, CreatePromise, resolve, el.Stop and
the loop drain API in the file.
This PR encapsulates the results of the daily performance and optimization audit for the TypeGo core bridge module. It systematically zeroes in on avoiding reflection/interface overhead and guarantees memory and synchronization safety.
Technical highlights:
bridge/core/reflection.go: Replaced allocation-heavy map key evaluation (MapKeys) with iterators (MapRange). Streamlined primitive formatting. Explicitly handled byte-slice conversion to direct memory bridging.eventloop/eventloop.go: Resolved a structural defect viasync.Oncewrapper inside promises, preventing multithreaded double-resolve panics.bridge/core/arraybuffer.go: PatchedToArrayBufferto force deep-copy behavior for strict engine isolation, mitigating memory stomps. Handled global context degradation gracefully by providing native struct fallback ifUint8Arrayconstructor vanishes in sandbox.All core benchmarks pass correctly, code has been audited via manual benchmarks, and safety annotations have been provided as requested.
PR created automatically by Jules for task 11718481340588092711 started by @repyh
Summary by CodeRabbit
Bug Fixes
Performance
Tests