Open jeromegn opened 6 years ago
For some reason, this doesn't work with all kinds of values... or something. I'm trying to make the whole ctx.Create use one handle scope, but started getting fun errors:
#
# Fatal error in HandleScope::HandleScope
# Entering the V8 API without proper locking in place
#
SIGILL: illegal instruction
PC=0x4a37942 m=0 sigcode=1
goroutine 0 [idle]:
runtime: unknown pc 0x4a37942
stack: frame={sp:0x7fff5fbfecf8, fp:0x0} stack=[0x7fff5fb7fa10,0x7fff5fbfee90)
00007fff5fbfebf8: 0000000006000000 00007fff5fbfed30
00007fff5fbfec08: 0000000000000000 00007fff5fbfecf0
00007fff5fbfec18: 0000000004a38010 000001da54b03389
// ...
^ This was while trying to create a Function (using a Bind function I made that doesn't acquire the scope), but I've seen it with a Date object too.
I was wrong, apparently that doesn't keep the locker or any other scope alive. It appears you don't need the locker or anything of the sort to create all kinds of values except Date and Function.
I don't think I can prevent the locker and scope destructors from firing when calling a different function. My C++ is pretty awful!
Update time: I was getting the scope errors because of switching threads.
I made a handleScope
which essentially queues functions to run in scope. The go ctx scope callback locks the OS thread and waits for "jobs" to run.
type handleScope chan func()
func (hs handleScope) Do(f func()) {
done := make(chan struct{}, 1)
hs <- func() {
f()
done <- struct{}{}
}
<-done
}
func (hs handleScope) End() {
close(hs)
}
Example usage:
func (ctx *Context) Create(val interface{}) (*Value, error) {
scope := ctx.WithScope()
var v *Value
var err error
scope.Do(func() {
v, _, err = ctx.create(reflect.ValueOf(val))
})
scope.End()
return v, err
}
This, is not super fast for 1 value. Hell, it's slower and allocates a bit more.
However, for a few values, it makes things faster.
func BenchmarkNewObjectWithoutScope(b *testing.B) {
ctx := NewIsolate().NewContext()
b.ResetTimer()
for n := 0; n < b.N; n++ {
ctx.NewObjectWoutScope()
ctx.NewObjectWoutScope()
ctx.NewObjectWoutScope()
ctx.NewObjectWoutScope()
ctx.NewObjectWoutScope()
ctx.NewObjectWoutScope()
}
}
func BenchmarkNewObjectWithScope(b *testing.B) {
ctx := NewIsolate().NewContext()
scope := ctx.WithScope() // don't need the chan here
todo := func() {
ctx.NewObjectWScope()
ctx.NewObjectWScope()
ctx.NewObjectWScope()
ctx.NewObjectWScope()
ctx.NewObjectWScope()
ctx.NewObjectWScope()
}
b.ResetTimer()
for n := 0; n < b.N; n++ {
scope.Do(todo)
}
}
$ go test -benchtime=10s -benchmem -run="^$" -bench "^BenchmarkNewObject"
goos: darwin
goarch: amd64
pkg: github.com/augustoroman/v8
BenchmarkNewObjectWithoutScope-4 1000000 65705 ns/op 192 B/op 6 allocs/op
BenchmarkNewObjectWithScope-4 1000000 20600 ns/op 320 B/op 8 allocs/op
PASS
ok github.com/augustoroman/v8 89.272s
There is a lot more lock contention with the scoped version. There's a for range on a channel that is single-threaded waiting for data.
For my use cases, this is pretty interesting. I have complex objects to pass and as soon as there are 2-3 sub-values needing creation, it can get pretty slow.
I can push to a branch if you're interested in seeing what's happening.
Ok, I woke up realizing that benchmark was unfair. I was not getting and putting the scope back during the benchmark, which the other one does.
With that in place, ns/op is very similar, but total ops is almost 10x lower with the batch mode. Which surprises me a lot! It is technically a few more cgo calls, but all in all, 2x less time is spent in cgo calls with the batch mode. It's spent elsewhere though, usually in runtime.
namespaced ops.
It's a bit hard to know which one is better in this very specific benchmark. I would think it'd be faster to prevent lock and scope changing threads all the time.
While I was fiddling with bindings in a different languages, I figured what was expensive was all the locking and scoping for every call. If you can instead callback into go, holding on a lock and/or a scope, you can really speed things up.
My thinking is we can batch all value creation (for structs and objects, etc.) or all logically grouped operations within a isolate lock and/or context scope with this.
Example implementation:
The C++ used:
Benchmarks:
Results:
Example usage:
edit: posted comment too quick by mistake, added code and examples.