I'm learning more about v8
internals as a hobby project. For this example, I'm trying to debug and understand how Javascript Map.prototype.set
actually works under-the-hood.
I'm using v8
tag 9.9.99
.
I first create a new Map
object in:
V8 version 9.9.99
d8> x = new Map()
[object Map]
d8> x.set(10,-10)
[object Map]
d8> %DebugPrint(x)
DebugPrint: 0x346c0804ad25: [JSMap]
- map: 0x346c08202771 <Map(HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x346c081c592d <Object map = 0x346c08202799>
- elements: 0x346c08002249 <FixedArray[0]> [HOLEY_ELEMENTS]
- table: 0x346c0804ad35 <OrderedHashMap[17]>
- properties: 0x346c08002249 <FixedArray[0]>
- All own properties (excluding elements): {}
When I break out of d8
into gdb
, I look into the table
attribute
gef➤ job 0x346c0804ad35
0x346c0804ad35: [OrderedHashMap]
- FixedArray length: 17
- elements: 1
- deleted: 0
- buckets: 2
- capacity: 4
- buckets: {
0: -1
1: 0
}
- elements: {
0: 10 -> -10
}
Poking around the v8
source, I find what I think to be the code related to OrderedHashTable
and OrderedHashMap
in src/objects/ordered-hash-table.cc
. Specifically, line 368:
MaybeHandle<OrderedHashMap> OrderedHashMap::Add(Isolate* isolate,
Handle<OrderedHashMap> table,
Handle<Object> key,
Handle<Object> value) {
...
After reading the code, my assumption is that OrderedHashMap::Add()
will get triggered when you do Map.Prototype.Set
(i.e., adding a new element). So I set a breakpoint here in and continue
gef➤ b v8::internal::OrderedHashMap::Add(v8::internal::Isolate*,
v8::internal::Handle<v8::internal::OrderedHashMap>,
v8::internal::Handle<v8::internal::Object>, v8::internal::Handle<v8::internal::Object>)
Breakpoint 1 at 0x557eb3b491e4
gef➤ c
Continuing.
I then attempt to set a new element, but the breakpoint does not trigger
d8> x.set(11,-11)
[object Map]
Breaking out into gdb
again, it appears the element has been added
gef➤ job 0x346c0804ad35
0x346c0804ad35: [OrderedHashMap]
- FixedArray length: 17
- elements: 2
- deleted: 0
- buckets: 2
- capacity: 4
- buckets: {
0: 1
1: 0
}
- elements: {
0: 10 -> -10
1: 11 -> -11
}
Do I have the breakpoint set up in the wrong spot? And if so, would anyone have any recommendations for efficiently finding the JS equivalents in v8
?
(V8 developer here.)
Many things in V8 have more than one implementation, for various reasons: in this case, there's the C++ way of adding an entry to an OrderedHashMap
(which you've found), and there's also a generated-code way of doing it. If you grep for MapPrototypeSet
, you'll find TF_BUILTIN(MapPrototypeSet, ...
in builtins-collections-gen.cc, which is the canonical implementation of Map.prototype.set
. Since that's a piece of code that runs at V8 build time to generate a "stub" which is then embedded into the binary, there's no direct way of setting a breakpoint into that stub. One way to do it is to insert a DebugBreak()
call into the stub-generating code, and recompile.
Not all builtins are implemented in the same way:
src/builtins/*-gen.cc
src/builtins/*.cc
src/builtins/*.tq
) which is V8's own DSL that translates to CSAsrc/runtime/*.cc
)Many have more than one implementation (typically a fully spec-compliant "slow" fallback, often but not always in C++, and then one or more fast paths that take shortcuts for common situations, often but not always in various forms of generated code). There are probably also a few special cases I'm forgetting right now; and as this post ages over the years, the enumeration above will become outdated (e.g. there used to be builtins in handwritten assembly, but we got rid of (almost all) of them; there used to be builtins generated by the old Crankshaft compiler, but we replaced that; there used to be builtins written in JavaScript, but we got rid of them; CSA was new at some point; Torque was new at some point; who knows what'll come next).
One consequence of all this is that questions like "how exactly does JavaScript's feature X work under the hood?" often don't have a concise answer.
Have fun with your investigation!