Search code examples
javascriptc++v8

v8 - how to debug Map.prototype.set and OrderedHashTable?


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?


Solution

  • (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:

    • some (like M.p.set) are generated "CSA builtins" in src/builtins/*-gen.cc
    • some are regular C++ in src/builtins/*.cc
    • some are written in Torque (src/builtins/*.tq) which is V8's own DSL that translates to CSA
    • some have fast paths directly in the compiler(s)
    • some have their meat in "runtime functions" (src/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!