// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

package nodev8 // import "go.opentelemetry.io/ebpf-profiler/interpreter/nodev8"

// Google V8 JavaScript Virtual Machine unwinder
//
// The code here is based to some extent on llnode (https://github.com/nodejs/llnode/)
// which is LLVM debugging extensions for node.js. A lot of additional insight is
// taken from V8 sources as it seems not many are doing things like this :)
//
// Core unwinding of frames is simple, as all the generated code uses frame pointers,
// and all the interesting data is directly via FP. This and collection of the related
// per-frame data is done in the eBPF code. See v8_tracer.ebpf.c for more details.
//
// The V8 generated code we are interested in resides in various locations:
//
//  1. Most of the on-the-fly code is in anonymous r-xp mappings. When V8 plugin is
//     attached. It will assume all such mappings are V8 code, and will install
//     PID page map entries for them on demand.
//
//  2. Pregenerated and bundled ("snapshot") code. That is nodejs build system
//     precompiles the support libraries, and bundles them as blob in the LOAD segment.
//     This is recognized as being a large gap in stack deltas (area without any
//     stack delta data) near the snapshot code. This area is hooked into using
//     the interpreter ranges mechanism.
//
// For the introspection data the V8 build generates a number of v8dbg_* symbols in
// the main binary. This has been always on for nodejs, and enabled also for chrome
// recently. These are generated by the gen-postmortem-metadata.py script using
// a heuristic analyzing the source. Unfortunately, this has caused the metadata to
// be incomplete, and have some symbols removed accidentally. So various fallback
// methods to get all relevant introspection data are needed.
//
// As minimal crash course, it is good to now that V8 uses Tagged machine words to
// represent practically all things. The two main to know about are the pointer
// to heap object (value looks like xxxxx01) and small integer (SMI) (the tag is
// like xxxxxx0; on 64-bit machines the value is 32-bits and located on the high
// bits, so it's more like xxxxxxxx00000000). All of the tag masks, values and
// shifts are in the introspection data - however, these are assumed fixed for
// the time being to generate faster and smaller code.
//
// Further all "heap objects" have basic runtime information including the object
// type Tag which can be used to identify the object type at the address.
//
// The object hierarchy and member names have changed over history quite a bit, but
// since V8 7.0 it seems to be a bit more stable. Also, the analysis is based on
// the Turbofan system where javascript is compiled first to bytecode and then to
// native code as needed. Earlier V8 used to always compile to native code and is
// quite a different thing.
//
// The basic tree of how we "chase" data from a stack frame (V8 7.8) is something like:
// Stack Frame
//  |- frame type (available in stub frames)
//  |- bytecode offset (for interpreted code)
//  \- JSFunction
//     |- Code
//     |  |- source_position_table
//     |  |    maps RIP to source code location
//     |  \- deoptimization_data
//     |       contains inlined function and on-stack-replacement, etc.
//     \- SharedFunctionInfo
//        |- name_or_scope_info (String or ScopeInfo)
//        |    as String, it should just contain the function name
//        |    as ScopeInfo (normal case), it contains the file name, and the
//        |       location of this scope along with other details.
//        |- function_data
//        |    can be BytecodeArray, interpreter.Data, UncompiledFunctionData, WASM data etc.
//        \- script_or_debug_info
//             as Script, it contains the text source code
//             as DebugInfo, there is also debug info available
//
// The basic info via introspection is normally just from the SharedFunctionInfo's
// ScopeInfo and Script. Those can be used to construct the function's name, source
// filename and location of the definition. Unfortunately the layout of ScopeInfo
// is highly volatile and does not have introspection data, thus a heuristic is
// used to grab the information from it.
// The Code object line tables further contain inlining information, together with
// the DeoptimizationData array.

// NodeJS options of interest:
// --turbo-profiling      force line info generation
// --no-turbo-inlining    disable inlining code in JIT functions
// --jitless              disable JITting
//
// NodeJS   V8 (possibly patched)
// 12.0.y   7.4.288
// 12.9.y   7.6.303
// 12.11.y  7.7.299
// 12.16.y  7.8.279
// 13.0.y   7.8.279
// 13.2.y   7.9.317
//          V8 8.1.0+ is supported by the host agent.
// 14.0.y   8.1.307
// 14.5.y   8.3.110
// 14.6.y   8.4.371
// 16.0.y   9.0.257    earliest supported version for arm64
// 16.4.y   9.1.269
// 16.6.y   9.2.230
// 16.11.y  9.4.19
// 18.9.y   10.2.154
// 20.1.y   11.3.244
// 21.1.y   11.8.172
// 22.0.y   12.4.254
// 23.0.y   12.9.202

// LIMITATIONS:
//  - Line number information is not always available. The V8 generates the LineNumber
//    information on demand (when the application first needs a backtrace), and the
//    NodeJS keeps some of the actual code in "External Strings" which are not easily
//    extractable. In practice, since node 16 the line numbers are usually available,
//    and in node 14 the line numbers usually available for user code (but not builtins).
//  - WebAssembly seems to have different frame format and data. Not supported yet.
//  - Asynchronous stack traces are not built
//    see: https://v8.dev/blog/fast-async
//         https://thecodebarbarian.com/async-stack-traces-in-node-js-12
//    The problem is that when "await" blocks, it will yield in coroutine manner,
//    and schedules deferred execution via microtask queue. This effectively,
//    means that the caller no longer on stack. The async callers need to be tracked
//    by explicitly walking the waiting microtask links. Native code is at:
//      src/inspector/v8-stack-trace-impl.cc: AsyncStackTrace::capture
//  - Line numbers from code in the embedded pre-compiled blob are not extracted.
//    (The PC is in code segment, and the corresponding JSFunction is in heap and it's
//     non trivial to get the PC delta.)

// Some source code references for further reading:
//   for upstream reference to build backtrace function names,
//     see: v8/src/objects/stack-frame-info.cc: AppendMethodCall().
//   for the extraction of the data from frame info,
//     see: v8/src/execution/messages.cc, class JSStackFrame impl.
//   for the building of frame info:
//     see: v8/src/execution/isolate.cc
//      - FrameArray::AppendJSFrame calls
//      - AppendJavaScriptFrame calls
//      - CaptureStackTrace function
//      - NeedsSourcePositionsForProfiling to determine when location information is generated
//   for the extraction of data from stack:
//     see: v8/src/execution/frames.cc
//      - *Frame::Summarize() methods
//      - InnerPointerToCodeCache::GetCacheEntry - RIP to Code mapping
//      - StackFrame::ComputeType
//   for generic RIP to Code object mapping
//     see: v8/src/heap/heap.cc:
//     - Heap::GcSafeFindCodeForInnerPointer
//        * InstructionStream::TryLookupCode lookups from embedded blob (requires
//          non-exposed internals)
//        * code_lo_space()->FindPage() for large page mappings
//        * Page::FromAddress() for standard heap page mappings
//     see: v8/src/objects/code.cc,h
//      - if Code object is known, can use instructions_start etc
//    for extracting the inlined functions from compiled code:
//     see: v8/src/deoptimizer/deoptimizer.cc, TranslatedState class and helpers
//      - TranslatedState::Init for decoding

import (
	"bytes"
	"errors"
	"fmt"
	"io"
	"reflect"
	"regexp"
	"sort"
	"strings"
	"sync/atomic"
	"unsafe"

	log "github.com/sirupsen/logrus"

	"github.com/elastic/go-freelru"

	"go.opentelemetry.io/ebpf-profiler/host"
	"go.opentelemetry.io/ebpf-profiler/interpreter"
	"go.opentelemetry.io/ebpf-profiler/libpf"
	"go.opentelemetry.io/ebpf-profiler/libpf/pfelf"
	"go.opentelemetry.io/ebpf-profiler/libpf/pfunsafe"
	"go.opentelemetry.io/ebpf-profiler/lpm"
	"go.opentelemetry.io/ebpf-profiler/metrics"
	npsr "go.opentelemetry.io/ebpf-profiler/nopanicslicereader"
	"go.opentelemetry.io/ebpf-profiler/process"
	"go.opentelemetry.io/ebpf-profiler/remotememory"
	"go.opentelemetry.io/ebpf-profiler/reporter"
	"go.opentelemetry.io/ebpf-profiler/successfailurecounter"
	"go.opentelemetry.io/ebpf-profiler/support"
	"go.opentelemetry.io/ebpf-profiler/util"
)

const (
	// Use build-time constants for the HeapObject/SMI Tags for code size and speed.
	// They are unlikely to change, and likely require larger modifications on change.
	SmiTag            = support.V8SmiTag
	SmiTagMask        = support.V8SmiTagMask
	SmiTagShift       = support.V8SmiTagShift
	SmiValueShift     = support.V8SmiValueShift
	HeapObjectTag     = support.V8HeapObjectTag
	HeapObjectTagMask = support.V8HeapObjectTagMask

	// The largest possible identifier for V8 frame type (marker)
	MaxFrameType = 64

	// The maximum fixed table size we accept to read. An arbitrarily selected
	// value to avoid huge malloc that could cause OOM crash.
	maximumFixedTableSize = 512 * 1024

	// lruSourceFileCacheSize is the LRU size for caching source files for an interpreter.
	// This should reflect the number of hot source files that are seen often in a trace.
	lruSourceFileCacheSize = 128

	// lruMapTypeCacheSize is the LRU size for caching the Map.InstanceType field.
	lruMapTypeCacheSize = 32

	// The native pointer size in bytes for 64-bit architectures
	pointerSize = 8
)

var (
	// regex for the interpreter executable or shared library
	v8Regex = regexp.MustCompile(`^(?:.*/)?(?:node|nsolid)(\d+)?$|^(?:.*/)libnode\.so(\.\d+)?$`)

	// the source file entry for unknown code blobs
	unknownSource = &v8Source{fileName: interpreter.UnknownSourceFile}

	// compiler check to make sure the needed interfaces are satisfied
	_ interpreter.Data     = &v8Data{}
	_ interpreter.Instance = &v8Instance{}
)

//nolint:lll
type v8Data struct {
	// vmStructs reflects the V8 internal class names and the offsets of named fields.
	// The V8 name is in the tag 'name' if it differs from our Go name.
	vmStructs struct {
		Fixed struct {
			// https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/tools/gen-postmortem-metadata.py#83
			// https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/include/v8-internal.h#38
			HeapObjectTagMask uint32
			SmiTagMask        uint32
			HeapObjectTag     uint16
			SmiTag            uint16 `zero:""`
			SmiShiftSize      uint16

			// https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/include/v8-internal.h#261
			FirstNonstringType uint16
			// https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/include/v8-internal.h#219
			StringEncodingMask uint16
			// https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/objects/instance-type.h#26
			StringRepresentationMask uint16
			SeqStringTag             uint16 `zero:""`
			ConsStringTag            uint16
			OneByteStringTag         uint16
			TwoByteStringTag         uint16 `zero:""`
			SlicedStringTag          uint16
			ThinStringTag            uint16

			// https://chromium.googlesource.com/v8/v8.git/+/ca3ef3fdf13f75475bcc964d7d79f5f7c66ea312/tools/gen-postmortem-metadata.py#82
			FirstJSFunctionType uint16
			LastJSFunctionType  uint16
		} `name:""`

		// https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/tools/gen-postmortem-metadata.py#182
		FramePointer struct {
			// https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/execution/frame-constants.h#70
			Function uint8 `name:"off_fp_function"`
			// https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/execution/frame-constants.h#118
			Context uint8 `name:"off_fp_context"`
			// https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/execution/frame-constants.h#332
			BytecodeArray uint8 `name:"off_fp_bytecode_array"`
			// https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/execution/frame-constants.h#334
			BytecodeOffset uint8 `name:"off_fp_bytecode_offset"`
		} `name:""`

		// https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/tools/gen-postmortem-metadata.py#195
		ScopeInfoIndex struct {
			// https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/objects/scope-info.h#258
			FirstVars uint8 `name:"scopeinfo_idx_first_vars"`
			// https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/objects/scope-info.h#247
			NContextLocals uint8 `name:"scopeinfo_idx_ncontextlocals"`
		} `name:""`

		// submitted upstream: https://chromium-review.googlesource.com/c/v8/v8/+/3902524
		DeoptimizationDataIndex struct {
			// https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/objects/code.h#912
			InlinedFunctionCount      uint8 `name:"DeoptimizationDataInlinedFunctionCountIndex"`
			LiteralArray              uint8 `name:"DeoptimizationDataLiteralArrayIndex"`
			SharedFunctionInfo        uint8 `name:"DeoptimizationDataSharedFunctionInfoIndex" zero:""`
			SharedFunctionInfoWrapper uint8 `name:"DeoptimizationDataSharedFunctionInfoWrapperIndex" zero:""`
			InliningPositions         uint8 `name:"DeoptimizationDataInliningPositionsIndex"`
		} `name:""`

		// https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/objects/code-kind.h#18
		CodeKind struct {
			// https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/objects/code.h#526
			// https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.5.2/tools/gen-postmortem-metadata.py#94
			FieldMask  uint32 `name:"CodeKindFieldMask" zero:""`
			FieldShift uint8  `name:"CodeKindFieldShift" zero:""`
			// https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/objects/code-kind.h#18
			// https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.5.2/tools/gen-postmortem-metadata.py#101
			Baseline uint8 `name:"CodeKindBaseline"`
		} `name:""`

		// https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/tools/gen-postmortem-metadata.py#341
		// https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/execution/frames.h#95
		FrameType struct {
			ArgumentsAdaptorFrame                       uint8
			BaselineFrame                               uint8
			BuiltinContinuationFrame                    uint8
			BuiltinExitFrame                            uint8
			BuiltinFrame                                uint8
			CWasmEntryFrame                             uint8
			ConstructEntryFrame                         uint8
			ConstructFrame                              uint8
			EntryFrame                                  uint8
			ExitFrame                                   uint8
			InternalFrame                               uint8
			InterpretedFrame                            uint8
			JavaScriptBuiltinContinuationFrame          uint8
			JavaScriptBuiltinContinuationWithCatchFrame uint8
			JavaScriptFrame                             uint8
			JsToWasmFrame                               uint8
			NativeFrame                                 uint8
			OptimizedFrame                              uint8
			StubFrame                                   uint8
			WasmCompileLazyFrame                        uint8
			WasmCompiledFrame                           uint8
			WasmExitFrame                               uint8
			WasmInterpreterEntryFrame                   uint8
			WasmToJsFrame                               uint8
		} `name:"frametype"`

		// https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/tools/gen-postmortem-metadata.py#709
		// https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/objects/instance-type.h#75
		Type struct {
			BaselineData              uint16 `name:"BaselineData__BASELINE_DATA_TYPE" zero:""`
			ByteArray                 uint16 `name:"ByteArray__BYTE_ARRAY_TYPE"`
			BytecodeArray             uint16 `name:"BytecodeArray__BYTECODE_ARRAY_TYPE"`
			Code                      uint16 `name:"Code__CODE_TYPE"`
			CodeWrapper               uint16 `name:"CodeWrapper__CODE_WRAPPER_TYPE" zero:""`
			FixedArray                uint16 `name:"FixedArray__FIXED_ARRAY_TYPE"`
			WeakFixedArray            uint16 `name:"WeakFixedArray__WEAK_FIXED_ARRAY_TYPE"`
			TrustedByteArray          uint16 `name:"TrustedByteArray__TRUSTED_BYTE_ARRAY_TYPE" zero:""`
			TrustedFixedArray         uint16 `name:"TrustedFixedArray__TRUSTED_FIXED_ARRAY_TYPE" zero:""`
			TrustedWeakFixedArray     uint16 `name:"TrustedFixedArray__TRUSTED_WEAK_FIXED_ARRAY_TYPE" zero:""`
			ProtectedFixedArray       uint16 `name:"ProtectedFixedArray__PROTECTED_FIXED_ARRAY_TYPE" zero:""`
			JSFunction                uint16 `name:"JSFunction__JS_FUNCTION_TYPE"`
			Map                       uint16 `name:"Map__MAP_TYPE"`
			Script                    uint16 `name:"Script__SCRIPT_TYPE"`
			ScopeInfo                 uint16 `name:"ScopeInfo__SCOPE_INFO_TYPE"`
			SharedFunctionInfo        uint16 `name:"SharedFunctionInfo__SHARED_FUNCTION_INFO_TYPE"`
			SharedFunctionInfoWrapper uint16 `name:"SharedFunctionInfoWrapper__SHARED_FUNCTION_INFO_WRAPPER_TYPE" zero:""`
		} `name:"type"`

		// https://chromium.googlesource.com/v8/v8.git/+/refs/tags/12.9.202.28/src/objects/shared-function-info.h#835
		SharedFunctionInfoWrapper struct {
			SharedFunctionInfo uint16 `name:"shared_info__Tagged_SharedFunctionInfo_" zero:""`
		}

		// https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/objects/heap-object.tq#7
		HeapObject struct {
			Map uint16 `name:"map__Map" zero:""`
		}

		// https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/objects/map.tq#37
		Map struct {
			InstanceType uint16 `name:"instance_type__uint16_t"`
		}

		// https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/objects/fixed-array.tq#7
		FixedArrayBase struct {
			Length uint16 `name:"length__SMI"`
		}

		// https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/objects/fixed-array.tq#14
		FixedArray struct {
			Data uint16 `name:"data__uintptr_t"`
		}

		// https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/objects/string.tq#10
		String struct {
			Length uint16 `name:"length__int32_t"`
		}

		// https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/objects/string.tq#108
		SeqOneByteString struct {
			Chars uint16 `name:"chars__char"`
		}

		// https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/objects/string.tq#114
		SeqTwoByteString struct {
			Chars uint16 `name:"chars__char"`
		}

		// https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/objects/string.tq#37
		ConsString struct {
			First  uint16 `name:"first__String"`
			Second uint16 `name:"second__String"`
		}

		// https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/objects/string.tq#129
		ThinString struct {
			Actual uint16 `name:"actual__String"`
		}

		// https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/objects/js-function.tq#23
		JSFunction struct {
			Code               uint16 `name:"code__Code,code__Tagged_Code_"`
			SharedFunctionInfo uint16 `name:"shared__SharedFunctionInfo"`
		}

		// https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/objects/code.h#467
		Code struct {
			DeoptimizationData  uint16 `name:"deoptimization_data__FixedArray,deoptimization_data__Tagged_FixedArray_"`
			SourcePositionTable uint16 `name:"source_position_table__ByteArray,source_position_table__Tagged_ByteArray_"`
			InstructionStart    uint16 `name:"instruction_start__uintptr_t,instruction_start__Address"`
			InstructionSize     uint16 `name:"instruction_size__int"`
			Flags               uint16 `name:"flags__uint32_t"`
		}

		// https://chromium.googlesource.com/v8/v8.git/+/refs/tags/12.9.202.28/src/objects/deoptimization-data.h#266
		DeoptimizationData struct {
			ProtectedFixedArray bool
			TrustedFixedArray   bool
			FixedArray          bool
		}

		// https://chromium.googlesource.com/v8/v8.git/+/refs/tags/12.9.202.28/src/objects/bytecode-array-inl.h#116
		SourcePositionTable struct {
			TrustedByteArray bool
			ByteArray        bool
		}

		// https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/objects/shared-function-info.tq#57
		SharedFunctionInfo struct {
			NameOrScopeInfo   uint16 `name:"name_or_scope_info__Object,name_or_scope_info__Tagged_Object_,name_or_scope_info__Tagged_NameOrScopeInfoT_"`
			FunctionData      uint16 `name:"function_data__Object,function_data__Tagged_Object_"`
			ScriptOrDebugInfo uint16 `name:"script_or_debug_info__Object,script_or_debug_info__HeapObject,script__Tagged_HeapObject_"`
		}

		// https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/objects/shared-function-info.tq#19
		BaselineData struct {
			Data uint16 `name:"data__Object" zero:""`
		}

		// https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/objects/code.tq#7
		BytecodeArray struct {
			SourcePositionTable uint16 `name:"source_position_table__Object,source_position_table__Tagged_HeapObject_"`
			Data                uint16 `name:"data__uintptr_t"`
		}

		// https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/objects/scope-info.h#41
		// https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/tools/gen-postmortem-metadata.py#39
		// The bool type indicates it's a parent relation type symbol.
		ScopeInfo struct {
			HeapObject bool
		}

		// class DeoptimizationLiteralArray introduced in V8 9.8.23
		// https://chromium.googlesource.com/v8/v8.git/+/refs/tags/10.2.154.1/src/objects/code.h#1090
		DeoptimizationLiteralArray struct {
			WeakFixedArray        bool
			TrustedWeakFixedArray bool
		}

		// https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/objects/script.tq#18
		Script struct {
			Name     uint16 `name:"name__Object"`
			LineEnds uint16 `name:"line_ends__Object"`
			Source   uint16 `name:"source__Object"`
		}
	}

	// snapshotRange is the LOAD segment area where V8 Snapshot code blob is
	snapshotRange util.Range

	// version contains the V8 version
	version uint32

	// bytecodeSizes contains the V8 bytecode length data
	bytecodeSizes []byte

	// bytecodeCount is the number of bytecode opcodes
	bytecodeCount uint8

	// frametypeToName caches frametype's name
	frametypeToName [MaxFrameType]libpf.String
}

type v8Instance struct {
	interpreter.InstanceStubs

	// Symbolization metrics
	successCount atomic.Uint64
	failCount    atomic.Uint64

	d  *v8Data
	rm remotememory.RemoteMemory

	// addrToString maps a V8 string object address to a Go string literal
	addrToString *freelru.LRU[libpf.Address, libpf.String]
	addrToSFI    *freelru.LRU[libpf.Address, *v8SFI]
	addrToCode   *freelru.LRU[libpf.Address, *v8Code]
	addrToSource *freelru.LRU[libpf.Address, *v8Source]
	addrToType   *freelru.LRU[libpf.Address, uint16]

	// mappings is indexed by the Mapping to its generation
	mappings map[process.Mapping]*uint32
	// prefixes is indexed by the prefix added to ebpf maps (to be cleaned up) to its generation
	prefixes map[lpm.Prefix]*uint32
	// mappingGeneration is the current generation (so old entries can be pruned)
	mappingGeneration uint32
}

// v8Source caches the data we need from V8 class Source
type v8Source struct {
	lineTable []uint32
	fileName  libpf.String
}

// v8Code caches the data we need from V8 class Code
type v8Code struct {
	sfi                 *v8SFI
	isBaseline          bool
	codeDeltaToPosition map[uint32]sourcePosition
	codePositionTable   []byte
	inliningSFIs        []byte
	inliningPositions   []byte
	cookie              uint32
}

// v8SFI caches the data we need from V8 class SharedFunctionInfo
type v8SFI struct {
	source                *v8Source
	bytecodeDeltaSeen     libpf.Set[uint32]
	bytecodePositionTable []byte
	bytecode              []byte
	funcName              libpf.String
	funcStartLine         libpf.SourceLineno
	funcStartPos          int
	funcEndPos            int
	bytecodeLength        uint32
}

func (i *v8Instance) Detach(ebpf interpreter.EbpfHandler, pid libpf.PID) error {
	err := ebpf.DeleteProcData(libpf.V8, pid)
	for prefix := range i.prefixes {
		if err2 := ebpf.DeletePidInterpreterMapping(pid, prefix); err2 != nil {
			err = errors.Join(err,
				fmt.Errorf("failed to remove page 0x%x/%d: %v",
					prefix.Key, prefix.Length, err2))
		}
	}
	if err != nil {
		return fmt.Errorf("failed to detach v8Instance from PID %d: %v",
			pid, err)
	}
	return nil
}

func (i *v8Instance) SynchronizeMappings(ebpf interpreter.EbpfHandler,
	_ reporter.ExecutableReporter, pr process.Process, mappings []process.Mapping) error {
	pid := pr.PID()
	i.mappingGeneration++
	for idx := range mappings {
		m := &mappings[idx]
		if !m.IsExecutable() || !m.IsAnonymous() {
			continue
		}

		if _, exists := i.mappings[*m]; exists {
			*i.mappings[*m] = i.mappingGeneration
			continue
		}

		// Generate a new uint32 pointer which is shared for mapping and the prefixes it owns
		// so updating the mapping above will reflect to prefixes also.
		mappingGeneration := i.mappingGeneration
		i.mappings[*m] = &mappingGeneration

		// Just assume all anonymous and executable mappings are V8 for now
		log.Debugf("Enabling V8 for %#x/%#x", m.Vaddr, m.Length)

		prefixes, err := lpm.CalculatePrefixList(m.Vaddr, m.Vaddr+m.Length)
		if err != nil {
			return fmt.Errorf("new anonymous mapping lpm failure %#x/%#x", m.Vaddr, m.Length)
		}

		for _, prefix := range prefixes {
			_, exists := i.prefixes[prefix]
			if !exists {
				err := ebpf.UpdatePidInterpreterMapping(pid, prefix, support.ProgUnwindV8, 0, 0)
				if err != nil {
					return err
				}
			}
			i.prefixes[prefix] = &mappingGeneration
		}
	}

	// Remove prefixes not seen
	for prefix, generationPtr := range i.prefixes {
		if *generationPtr == i.mappingGeneration {
			continue
		}
		log.Debugf("Delete V8 prefix %#v", prefix)
		_ = ebpf.DeletePidInterpreterMapping(pid, prefix)
		delete(i.prefixes, prefix)
	}
	for m, generationPtr := range i.mappings {
		if *generationPtr == i.mappingGeneration {
			continue
		}
		log.Debugf("Disabling V8 for %#x/%#x", m.Vaddr, m.Length)
		delete(i.mappings, m)
	}

	return nil
}

func (i *v8Instance) GetAndResetMetrics() ([]metrics.Metric, error) {
	addrToStringStats := i.addrToString.ResetMetrics()
	addrToSFIStats := i.addrToSFI.ResetMetrics()
	addrToCodeStats := i.addrToCode.ResetMetrics()
	addrToSourceStats := i.addrToSource.ResetMetrics()

	return []metrics.Metric{
		{
			ID:    metrics.IDV8SymbolizationSuccess,
			Value: metrics.MetricValue(i.successCount.Swap(0)),
		},
		{
			ID:    metrics.IDV8SymbolizationFailure,
			Value: metrics.MetricValue(i.failCount.Swap(0)),
		},
		{
			ID:    metrics.IDV8AddrToStringHit,
			Value: metrics.MetricValue(addrToStringStats.Hits),
		},
		{
			ID:    metrics.IDV8AddrToStringMiss,
			Value: metrics.MetricValue(addrToStringStats.Misses),
		},
		{
			ID:    metrics.IDV8AddrToStringAdd,
			Value: metrics.MetricValue(addrToStringStats.Inserts),
		},
		{
			ID:    metrics.IDV8AddrToStringDel,
			Value: metrics.MetricValue(addrToStringStats.Removals),
		},
		{
			ID:    metrics.IDV8AddrToSFIHit,
			Value: metrics.MetricValue(addrToSFIStats.Hits),
		},
		{
			ID:    metrics.IDV8AddrToSFIMiss,
			Value: metrics.MetricValue(addrToSFIStats.Misses),
		},
		{
			ID:    metrics.IDV8AddrToSFIAdd,
			Value: metrics.MetricValue(addrToSFIStats.Inserts),
		},
		{
			ID:    metrics.IDV8AddrToSFIDel,
			Value: metrics.MetricValue(addrToSFIStats.Removals),
		},
		{
			ID:    metrics.IDV8AddrToFuncHit,
			Value: metrics.MetricValue(addrToCodeStats.Hits),
		},
		{
			ID:    metrics.IDV8AddrToFuncMiss,
			Value: metrics.MetricValue(addrToCodeStats.Misses),
		},
		{
			ID:    metrics.IDV8AddrToFuncAdd,
			Value: metrics.MetricValue(addrToCodeStats.Inserts),
		},
		{
			ID:    metrics.IDV8AddrToFuncDel,
			Value: metrics.MetricValue(addrToCodeStats.Removals),
		},
		{
			ID:    metrics.IDV8AddrToSourceHit,
			Value: metrics.MetricValue(addrToSourceStats.Hits),
		},
		{
			ID:    metrics.IDV8AddrToSourceMiss,
			Value: metrics.MetricValue(addrToSourceStats.Misses),
		},
		{
			ID:    metrics.IDV8AddrToSourceAdd,
			Value: metrics.MetricValue(addrToSourceStats.Inserts),
		},
		{
			ID:    metrics.IDV8AddrToSourceDel,
			Value: metrics.MetricValue(addrToSourceStats.Removals),
		},
	}, nil
}

// v8Ver encodes the x.y.z version to single uint32
func v8Ver(x, y, z uint32) uint32 {
	return (x << 24) + (y << 16) + z
}

// isSMI tests if the machine word is a V8 SMI (SMall Integer)
func isSMI(val uint64) bool {
	return val&SmiTagMask == SmiTag
}

// decideSMI extracts the integer value from a SMI. Returns zero for bad tag.
func decodeSMI(val uint64) uint32 {
	if !isSMI(val) {
		return 0
	}
	return uint32(val >> SmiValueShift)
}

// isHeapObject tests if the given address is a valid tagged pointer
func isHeapObject(val libpf.Address) bool {
	return val&HeapObjectTagMask == HeapObjectTag
}

// symbolizeMarkerFrame symbolizes and adds to trace a V8 stub frame
func (i *v8Instance) symbolizeMarkerFrame(marker uint64, frames *libpf.Frames) error {
	if marker >= MaxFrameType {
		return fmt.Errorf("v8 tracer returned invalid marker: %d", marker)
	}

	stubName := i.d.frametypeToName[marker]
	if stubName == libpf.NullString {
		name := "V8::UnknownFrame"
		frameTypesType := reflect.TypeOf(&i.d.vmStructs.FrameType).Elem()
		frameTypesValue := reflect.ValueOf(&i.d.vmStructs.FrameType).Elem()
		for i := 0; i < frameTypesValue.NumField(); i++ {
			if frameTypesValue.Field(i).Uint() == marker {
				name = "V8::" + frameTypesType.Field(i).Name
				break
			}
		}
		stubName = libpf.Intern(name)
		i.d.frametypeToName[marker] = stubName
	}

	frames.Append(&libpf.Frame{
		Type:         libpf.V8Frame,
		FunctionName: stubName,
	})
	return nil
}

// getObjectAddrAndType validates tagged pointer and reads its object tag.
// On return, the actual address and its type tag are returned, or an error.
func (i *v8Instance) getObjectAddrAndType(taggedPtr libpf.Address) (libpf.Address, uint16, error) {
	vms := &i.d.vmStructs
	if !isHeapObject(taggedPtr) {
		return 0, 0, fmt.Errorf("%#x is not a tagged pointer", taggedPtr)
	}
	addr := taggedPtr &^ HeapObjectTagMask
	taggedMapAddr := i.rm.Ptr(addr + libpf.Address(vms.HeapObject.Map))
	if taggedMapAddr == 0 || !isHeapObject(taggedMapAddr) {
		return 0, 0, fmt.Errorf("object map for %#x is not a valid heap pointer", taggedPtr)
	}

	if instanceType, ok := i.addrToType.Get(taggedMapAddr); ok {
		return addr, instanceType, nil
	}

	mapAddr := taggedMapAddr &^ HeapObjectTagMask
	instanceType := i.rm.Uint16(mapAddr + libpf.Address(vms.Map.InstanceType))
	if instanceType != 0 {
		i.addrToType.Add(taggedMapAddr, instanceType)
	}
	return addr, instanceType, nil
}

// getTypedObject checks the object's type, and returns its address or error.
func (i *v8Instance) getTypedObject(taggedPtr libpf.Address, expectedType uint16) (
	libpf.Address, error) {
	addr, tag, err := i.getObjectAddrAndType(taggedPtr)
	if err != nil {
		return 0, err
	}
	if tag != expectedType {
		return 0, fmt.Errorf("%#x instance is %#x, but expected %#x", addr, tag, expectedType)
	}
	return addr, nil
}

// readObjectPtr reads an object pointer, and parses it as a HeapObject pointer.
func (i *v8Instance) readObjectPtr(addr libpf.Address) (libpf.Address, uint16, error) {
	return i.getObjectAddrAndType(i.rm.Ptr(addr))
}

// readTypedObjectPtr reads an object pointer and makes sure it is a HeapObject of expected type
func (i *v8Instance) readTypedObjectPtr(addr libpf.Address, expectedType uint16) (
	libpf.Address, error) {
	addr, tag, err := i.readObjectPtr(addr)
	if err != nil {
		return 0, err
	}
	if tag != expectedType {
		return 0, fmt.Errorf("%#x instance is %#x, but expected %#x", addr, tag, expectedType)
	}
	return addr, nil
}

// extractString reads string from given address. If the object type tag is given, the pointer
// can be tagged or not. Zero tag can be used to first read and validate the string tag, in this
// case the pointer is expected to be in the tagged format.
// The extracted string can be large (e.g. entire files of source code), and is extracted in
// fragments. Some V8 string representations (e.g. ConsString) is naturally fragmented, but this
// code will also internally split long continuous string literals to fragments to avoid large
// memory usage.
func (i *v8Instance) extractString(ptr libpf.Address, tag uint16, cb func(string) error) error {
	var err error

	vms := &i.d.vmStructs
	if tag == 0 {
		ptr, tag, err = i.getObjectAddrAndType(ptr)
		if err != nil {
			return err
		}
	}

	if tag >= vms.Fixed.FirstNonstringType {
		return fmt.Errorf("not a string at %#x, tag is %#x", ptr, tag)
	}

	switch tag & vms.Fixed.StringRepresentationMask {
	case vms.Fixed.SeqStringTag:
		length := i.rm.Uint32(ptr + libpf.Address(vms.String.Length))
		switch tag & vms.Fixed.StringEncodingMask {
		case vms.Fixed.OneByteStringTag:
			bufSz := min(uint32(16*1024), length)
			buf := make([]byte, bufSz)
			for offs := uint32(0); offs < length; offs += bufSz {
				if length-offs < bufSz {
					buf = buf[0 : length-offs]
				}
				err = i.rm.Read(ptr+
					libpf.Address(vms.SeqOneByteString.Chars)+
					libpf.Address(offs),
					buf)
				if err != nil {
					return err
				}
				if err = cb(pfunsafe.ToString(buf)); err != nil {
					return err
				}
			}
		case vms.Fixed.TwoByteStringTag:
			return errors.New("two byte string not supported")
		default:
			return fmt.Errorf("unsupported encoding: %#x", tag)
		}
	case vms.Fixed.ConsStringTag:
		if err = i.extractStringPtr(ptr+libpf.Address(vms.ConsString.First),
			cb); err != nil {
			return err
		}
		if err = i.extractStringPtr(ptr+libpf.Address(vms.ConsString.Second),
			cb); err != nil {
			return err
		}
	case vms.Fixed.ThinStringTag:
		return i.extractStringPtr(ptr+libpf.Address(vms.ThinString.Actual), cb)
	default:
		return fmt.Errorf("unsupported string tag %#x", tag&vms.Fixed.StringRepresentationMask)
	}
	return nil
}

func (i *v8Instance) extractStringPtr(ptr libpf.Address, cb func(string) error) error {
	return i.extractString(i.rm.Ptr(ptr), 0, cb)
}

// getString extracts and caches a small string object from given address.
func (i *v8Instance) getString(ptr libpf.Address, tag uint16) (libpf.String, error) {
	taggedPtr := ptr | HeapObjectTag
	if value, ok := i.addrToString.Get(taggedPtr); ok {
		return value, nil
	}

	str := ""
	err := i.extractString(ptr, tag, func(fragment string) error {
		// 1kB maximum for file, function and class names
		if len(str)+len(fragment) >= 1024 {
			return fmt.Errorf("string too long (at least %d+%d)",
				len(str), len(fragment))
		}
		str += fragment
		return nil
	})
	if err != nil {
		return libpf.NullString, err
	}
	if str != "" && !util.IsValidString(str) {
		return libpf.NullString, fmt.Errorf("invalid string at 0x%x", ptr)
	}

	value := libpf.Intern(str)
	i.addrToString.Add(taggedPtr, value)
	return value, nil
}

// getStringPtr reads a V8 string pointer and dereferences it.
func (i *v8Instance) getStringPtr(ptr libpf.Address) (libpf.String, error) {
	return i.getString(i.rm.Ptr(ptr), 0)
}

// analyzeScopeInfo reads and heuristically analyzes V8 ScopeInfo data. It tries to
// extract the function name, and its start and end line.
func (i *v8Instance) analyzeScopeInfo(ptr libpf.Address) (name libpf.String,
	startPos, endPos int) {
	vms := &i.d.vmStructs
	var data libpf.Address
	if vms.ScopeInfo.HeapObject {
		// ScopeInfo is HeapObject based (since V8 9.1.71)
		data = ptr + libpf.Address(vms.HeapObject.Map+pointerSize)
	} else {
		// ScopeInfo is FixedArray based (before V8 9.1.71)
		data = ptr + libpf.Address(vms.FixedArray.Data)
	}

	// The maximum number of 'slots' inspected. Based on assumptions on what
	// data can be in the array before function name and line numbers. The exact
	// number varies V8 version to version. Counting the actual slot number
	// would require tracking lot of V8 bitfields that have changed a lot, see:
	//nolint:lll
	// https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/objects/scope-info.tq#50
	const numSlots = 16
	const slotSize = pointerSize
	slotData := make([]byte, numSlots*slotSize)
	if err := i.rm.Read(data, slotData); err != nil {
		return libpf.NullString, 0, 0
	}

	// Skip reserved slots and the context locals
	ndx := int(vms.ScopeInfoIndex.FirstVars)
	ndx += 2 * int(decodeSMI(npsr.Uint64(slotData,
		uint(vms.ScopeInfoIndex.NContextLocals)*slotSize)))

	name = libpf.NullString
	prev := uint64(HeapObjectTag)
	for ; ndx < numSlots; ndx++ {
		cur := npsr.Uint64(slotData, uint(ndx*slotSize))
		if name == libpf.NullString && isHeapObject(libpf.Address(cur)) {
			// Just try getting the string ignoring errors and
			// assume that first valid string is the function name
			name, _ = i.getString(libpf.Address(cur), 0)
		}
		if isSMI(cur) && isSMI(prev) {
			// Assume that two numbers (first one lower than the second)
			// is the start/end position pair. This also follows after
			// function name, so break when found.
			startPos = int(decodeSMI(prev))
			endPos = int(decodeSMI(cur))
			if startPos < endPos {
				return name, startPos, endPos
			}
		}
		prev = cur
	}
	return name, 0, 0
}

// readFixedTable reads the data of a FixedArray object.
func (i *v8Instance) readFixedTable(addr libpf.Address, itemSize, maxItems uint32) ([]byte, error) {
	vms := &i.d.vmStructs

	numItems := decodeSMI(i.rm.Uint64(addr + libpf.Address(vms.FixedArrayBase.Length)))
	if maxItems != 0 && numItems > maxItems {
		numItems = maxItems
	}

	size := numItems * itemSize
	if size == 0 || size >= maximumFixedTableSize {
		return nil, fmt.Errorf("fixed table size: %d", size)
	}

	data := make([]byte, size)
	err := i.rm.Read(addr+libpf.Address(vms.FixedArray.Data), data)
	if err != nil {
		return nil, fmt.Errorf("fixed table: %w", err)
	}

	return data, nil
}

// readFixedTablePtr read the data of a FixedArray object.
func (i *v8Instance) readFixedTablePtr(taggedPtr libpf.Address, tag uint16,
	itemSize, maxItems uint32) ([]byte, error) {
	addr, err := i.readTypedObjectPtr(taggedPtr, tag)
	if err != nil {
		return nil, err
	}
	return i.readFixedTable(addr, itemSize, maxItems)
}

// getSource reads and caches needed V8 Source object data.
func (i *v8Instance) getSource(addr libpf.Address) *v8Source {
	if value, ok := i.addrToSource.Get(addr); ok {
		return value
	}

	vms := &i.d.vmStructs

	var err error
	src := &v8Source{}
	src.fileName, _ = i.getStringPtr(addr + libpf.Address(vms.Script.Name))
	if vms.Script.LineEnds != 0 {
		// First read the LineEnds directly if available
		var data []byte
		data, err = i.readFixedTablePtr(
			addr+libpf.Address(vms.Script.LineEnds),
			vms.Type.FixedArray, 8, 0)
		log.Debugf("Reading LineEnds: %d: %v", len(data), err)
		if err == nil {
			lines := make([]uint32, len(data)/8)
			for i := range lines {
				val := npsr.Uint64(data, uint(i*8))
				lines[i] = decodeSMI(val)
			}
			src.lineTable = lines
		}
	}
	if src.lineTable == nil {
		// Try reading the full source to calculate line ends
		ends := make([]uint32, 0, 100)
		prev := byte(0)
		fragStart := 0
		err = i.extractStringPtr(addr+libpf.Address(vms.Script.Source),
			func(fragment string) error {
				for i := 0; i < len(fragment); i++ {
					ch := fragment[i]
					if ch == '\r' || (ch == '\n' && prev != '\r') {
						ends = append(ends, uint32(fragStart+i))
					}
					prev = ch
				}
				fragStart += len(fragment)
				return nil
			})
		log.Debugf("Reading Source: %d lines: %v", len(ends), err)
		if err == nil && len(ends) > 0 {
			src.lineTable = ends
		}
	}

	i.addrToSource.Add(addr, src)
	return src
}

// getSFI reads and caches needed V8 SharedFunctionInfo object data.
func (i *v8Instance) getSFI(taggedPtr libpf.Address) (*v8SFI, error) {
	if value, ok := i.addrToSFI.Get(taggedPtr); ok {
		return value, nil
	}

	vms := &i.d.vmStructs
	addr, err := i.getTypedObject(taggedPtr, vms.Type.SharedFunctionInfo)
	if err != nil {
		return nil, fmt.Errorf("failed to read SFI: %w", err)
	}

	// Read the function name
	nosAddr, nosType, err := i.readObjectPtr(addr +
		libpf.Address(vms.SharedFunctionInfo.NameOrScopeInfo))
	if err != nil {
		return nil, err
	}
	sfi := &v8SFI{
		source:            unknownSource,
		bytecodeDeltaSeen: make(libpf.Set[uint32]),
	}
	switch {
	case nosType == vms.Type.ScopeInfo:
		sfi.funcName, sfi.funcStartPos, sfi.funcEndPos = i.analyzeScopeInfo(nosAddr)
	case nosType < vms.Fixed.FirstNonstringType:
		sfi.funcName, err = i.getString(nosAddr, nosType)
	}
	if err != nil {
		sfi.funcName = libpf.Intern(fmt.Sprintf("<%s>", err))
	}
	if sfi.funcName == libpf.NullString {
		sfi.funcName = interpreter.AnonymousFunctionName
	}

	// Function data
	//nolint:lll
	// https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/objects/shared-function-info.cc#76
	fdAddr, fdType, _ := i.readObjectPtr(addr + libpf.Address(vms.SharedFunctionInfo.FunctionData))
	switch fdType {
	case vms.Type.BaselineData:
		fdAddr, fdType, _ = i.readObjectPtr(fdAddr + libpf.Address(vms.BaselineData.Data))
	case vms.Type.Code:
		// DeoptimizationData is really Bytecode for Baseline code
		fdAddr, fdType, _ = i.readObjectPtr(fdAddr + libpf.Address(vms.Code.DeoptimizationData))
	}
	if fdType == vms.Type.BytecodeArray {
		length := decodeSMI(i.rm.Uint64(fdAddr + libpf.Address(vms.FixedArrayBase.Length)))
		sfi.bytecodeLength = length
		if length > 0 && length < 512*1024 && vms.BytecodeArray.Data != 0 {
			log.Debugf("Bytecode available, %d bytes", length)
			sfi.bytecode = make([]byte, length)
			err = i.rm.Read(fdAddr+libpf.Address(vms.BytecodeArray.Data), sfi.bytecode)
			if err != nil {
				return nil, err
			}
		} else {
			log.Debugf("Bytecode, %d bytes, not available", length)
		}
		sfi.bytecodePositionTable, err = i.readFixedTablePtr(
			fdAddr+libpf.Address(vms.BytecodeArray.SourcePositionTable),
			vms.Type.ByteArray, 1, 0)
		log.Debugf("Bytecode positions: %d bytes: %v", len(sfi.bytecodePositionTable), err)
	}

	// Script
	sodiAddr, sodiType, _ := i.readObjectPtr(addr +
		libpf.Address(vms.SharedFunctionInfo.ScriptOrDebugInfo))
	if sodiType == vms.Type.Script {
		sfi.source = i.getSource(sodiAddr)
		if sfi.funcStartPos != sfi.funcEndPos {
			sfi.funcStartLine = mapPositionToLine(sfi.source.lineTable,
				int32(sfi.funcStartPos))
		}
	}

	log.Debugf("SFI %#x: name: %v, start/end: %v/%v, file/line: %v:%v, #sourceLines: %d",
		taggedPtr, sfi.funcName, sfi.funcStartPos, sfi.funcEndPos,
		sfi.source.fileName, sfi.funcStartLine, len(sfi.source.lineTable))

	i.addrToSFI.Add(taggedPtr, sfi)
	return sfi, nil
}

// readCode reads and caches needed V8 Code object data.
func (i *v8Instance) readCode(taggedPtr libpf.Address, cookie uint32, sfi *v8SFI) (*v8Code, error) {
	vms := &i.d.vmStructs

	codeAddr, err := i.getTypedObject(taggedPtr, vms.Type.Code)
	if err != nil {
		return nil, fmt.Errorf("code pointer read: %v", err)
	}

	// Read the class Code contained data up to the largest offset we need
	// This follows the layout described in vms.Code:
	// https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/objects/code.h#467
	codeSize := vms.Code.Flags + 4
	code := make([]byte, codeSize)
	err = i.rm.Read(codeAddr, code)
	if err != nil {
		return nil, fmt.Errorf("code object read: %v", err)
	}

	// Code Kind
	codeFlags := npsr.Uint32(code, uint(vms.Code.Flags))
	codeKind := uint8((codeFlags & vms.CodeKind.FieldMask) >> vms.CodeKind.FieldShift)

	// Read in full source position tables
	sourcePositionPtr := npsr.Ptr(code, uint(vms.Code.SourcePositionTable))
	if vms.SourcePositionTable.TrustedByteArray {
		sourcePositionPtr, err = i.getTypedObject(sourcePositionPtr, vms.Type.TrustedByteArray)
	} else {
		sourcePositionPtr, err = i.getTypedObject(sourcePositionPtr, vms.Type.ByteArray)
	}
	if err != nil {
		return nil, fmt.Errorf("code source position pointer read: %v", err)
	}
	codePositionTable, err := i.readFixedTable(sourcePositionPtr, 1, 0)
	if err != nil {
		return nil, fmt.Errorf("code source position table read: %v", err)
	}

	// Baseline Code does not have deoptimization data
	if codeKind == vms.CodeKind.Baseline {
		if sfi == nil {
			return nil, errors.New("baseline function without SFI")
		}

		log.Debugf("Baseline Code %#x read: posSize: %v, cookie: %x",
			codeAddr, len(codePositionTable), cookie)

		v8code := &v8Code{
			codeDeltaToPosition: make(map[uint32]sourcePosition),
			sfi:                 sfi,
			isBaseline:          true,
			codePositionTable:   codePositionTable,
			cookie:              cookie,
		}
		i.addrToCode.Add(taggedPtr, v8code)
		return v8code, nil
	}

	// Read the deoptimization data
	deoptimizationDataPtr := npsr.Ptr(code, uint(vms.Code.DeoptimizationData))
	if vms.DeoptimizationData.ProtectedFixedArray {
		deoptimizationDataPtr, err =
			i.getTypedObject(deoptimizationDataPtr, vms.Type.ProtectedFixedArray)
	} else if vms.DeoptimizationData.TrustedFixedArray {
		deoptimizationDataPtr, err =
			i.getTypedObject(deoptimizationDataPtr, vms.Type.TrustedFixedArray)
	} else {
		deoptimizationDataPtr, err =
			i.getTypedObject(deoptimizationDataPtr, vms.Type.FixedArray)
	}
	if err != nil {
		return nil, fmt.Errorf("deoptimization data pointer read: %v", err)
	}
	// We don't have metadata for TrustedFixedArray (at least as of 12.3.219),
	// but for now the layout is the same as FixedArray, so it's fine if what follows
	// assumes FixedArray.
	numItemsNeeded := uint32(vms.DeoptimizationDataIndex.InliningPositions + 1)
	deoptimizationData, err := i.readFixedTable(deoptimizationDataPtr, pointerSize, numItemsNeeded)
	if err != nil {
		return nil, fmt.Errorf("deoptimization pointer read: %v", err)
	}
	if len(deoptimizationData) < pointerSize*int(numItemsNeeded) {
		return nil, fmt.Errorf("DeoptimizationData array length too small: %d",
			len(deoptimizationData))
	}

	if sfi == nil {
		// Read the Code's SFI
		var sfiPtr libpf.Address
		if vms.DeoptimizationDataIndex.SharedFunctionInfoWrapper != 0 {
			sfiWrapperPtr := npsr.Ptr(deoptimizationData,
				uint(vms.DeoptimizationDataIndex.SharedFunctionInfoWrapper*pointerSize))
			sfiWrapperPtr, err = i.getTypedObject(sfiWrapperPtr, vms.Type.SharedFunctionInfoWrapper)
			if err != nil {
				return nil, fmt.Errorf("sfi wrapper pointer read: %w", err)
			}

			sfiWrapperSize := vms.SharedFunctionInfoWrapper.SharedFunctionInfo + pointerSize
			sfiWrapper := make([]byte, sfiWrapperSize)
			if err = i.rm.Read(sfiWrapperPtr, sfiWrapper); err != nil {
				return nil, fmt.Errorf("sfi wrapper read: %w", err)
			}
			sfiPtr = npsr.Ptr(sfiWrapper, uint(vms.SharedFunctionInfoWrapper.SharedFunctionInfo))
		} else {
			sfiPtr = npsr.Ptr(deoptimizationData,
				uint(vms.DeoptimizationDataIndex.SharedFunctionInfo*pointerSize))
		}
		sfi, err = i.getSFI(sfiPtr)
		if err != nil {
			return nil, fmt.Errorf("getSFI: %w", err)
		}
	}

	// Read the Code's deoptimization data
	numSFI := decodeSMI(npsr.Uint64(deoptimizationData,
		uint(vms.DeoptimizationDataIndex.InlinedFunctionCount*pointerSize)))

	var inliningSFIs, inliningPositions []byte
	if numSFI > 0 {
		// The first numSFI entries of literal array are the pointers for
		// inlined function's SFI structures
		expectedTag := vms.Type.FixedArray
		if vms.DeoptimizationLiteralArray.TrustedWeakFixedArray {
			expectedTag = vms.Type.TrustedWeakFixedArray
		} else if vms.DeoptimizationLiteralArray.WeakFixedArray {
			expectedTag = vms.Type.WeakFixedArray
		}
		literalArrayPtr := npsr.Ptr(deoptimizationData,
			uint(vms.DeoptimizationDataIndex.LiteralArray*pointerSize))
		literalArrayPtr, err = i.getTypedObject(literalArrayPtr, expectedTag)
		if err != nil {
			return nil, fmt.Errorf("literal array pointer read: %v", err)
		}

		inliningSFIs, err = i.readFixedTable(literalArrayPtr, pointerSize, numSFI)
		if err != nil {
			return nil, fmt.Errorf("literal array data read: %v", err)
		}

		// Read the complete inlining positions structure
		inliningPositionsPtr := npsr.Ptr(deoptimizationData,
			uint(vms.DeoptimizationDataIndex.InliningPositions*pointerSize))
		inliningPositionsPtr, err = i.getTypedObject(inliningPositionsPtr, vms.Type.ByteArray)
		if err != nil {
			return nil, fmt.Errorf("inlining position pointer read: %v", err)
		}
		inliningPositions, err = i.readFixedTable(inliningPositionsPtr, 1, 0)
		if err != nil {
			return nil, fmt.Errorf("inlining position data read: %v", err)
		}
	}

	log.Debugf("Code %#x read: posSize: %v, sfiSize: %v, inlineSize: %v cookie: %x",
		codeAddr, len(codePositionTable), len(inliningSFIs),
		len(inliningPositions), cookie)

	v8code := &v8Code{
		codeDeltaToPosition: make(map[uint32]sourcePosition),
		sfi:                 sfi,
		codePositionTable:   codePositionTable,
		inliningSFIs:        inliningSFIs,
		inliningPositions:   inliningPositions,
		cookie:              cookie,
	}
	i.addrToCode.Add(taggedPtr, v8code)
	return v8code, nil
}

// getCode reads and caches needed V8 Code object data from a Code pointer.
func (i *v8Instance) getCode(taggedPtr libpf.Address, cookie uint32) (*v8Code, error) {
	if code, ok := i.addrToCode.Get(taggedPtr); ok {
		if code.cookie == cookie {
			return code, nil
		}
		i.addrToCode.Remove(taggedPtr)
	}
	return i.readCode(taggedPtr, cookie, nil)
}

// getCodeFromJSFunction reads and caches needed V8 Code object data from a JSFunction pointer.
func (i *v8Instance) getCodeFromJSFunc(taggedPtr libpf.Address, cookie uint32) (*v8Code, error) {
	if code, ok := i.addrToCode.Get(taggedPtr); ok {
		if code.cookie == cookie {
			return code, nil
		}
		i.addrToCode.Remove(taggedPtr)
	}

	vms := &i.d.vmStructs
	jsfuncAddr := taggedPtr &^ HeapObjectTagMask

	// Read needed JSFunction object data
	jsfuncSize := max(vms.JSFunction.SharedFunctionInfo, vms.JSFunction.Code) + pointerSize
	jsfunc := make([]byte, jsfuncSize)
	err := i.rm.Read(jsfuncAddr, jsfunc)
	if err != nil {
		return nil, fmt.Errorf("jsfunc object read: %v", err)
	}

	sfi, err := i.getSFI(npsr.Ptr(jsfunc, uint(vms.JSFunction.SharedFunctionInfo)))
	if err != nil {
		return nil, fmt.Errorf("getSFI: %w", err)
	}

	// Chase and read the Code object
	codeTaggedPtr := npsr.Ptr(jsfunc, uint(vms.JSFunction.Code))
	return i.readCode(codeTaggedPtr, cookie, sfi)
}

// decodeUVLQ reads and decodes one unsigned Variable Length Quantity
// https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/base/vlq.h#104
func decodeUVLQ(r io.ByteReader) (uint64, error) {
	// Base-128 or variable-length decoding.
	// MSB indicates if there's more bytes to be read or not.
	decoded := uint64(0)
	for shift := 0; true; shift += 7 {
		cur, err := r.ReadByte()
		if err != nil {
			return 0, err
		}
		decoded |= uint64(cur&0x7f) << shift
		if cur&0x80 == 0 {
			break
		}
	}
	return decoded, nil
}

// decodeVLQ reads and decodes one signed Variable Length Quantity
// https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/base/vlq.h#110
// https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/codegen/source-position-table.cc#90
//
//nolint:lll
func decodeVLQ(r io.ByteReader) (int64, error) {
	decoded, err := decodeUVLQ(r)
	if err != nil {
		return 0, err
	}
	// Sign decoding: LSB determines sign
	return int64((decoded >> 1) ^ -(decoded & 1)), nil
}

// https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/codegen/source-position.h#145
//
// sourcePositition a native V8 datatype defined as a bitfield with following bits:
//
//	is_external       1 bit
//	IF is_external {
//	  external_line   20 bits
//	  external_file   10 bits
//	} ELSE {
//	  script_offset   30 bits
//	}
//	inlining_id       16 bits
//
//nolint:lll
type sourcePosition uint64

func (pos sourcePosition) isExternal() bool {
	return pos&1 == 1
}

func (pos sourcePosition) inliningID() uint16 {
	return uint16(pos >> 31)
}

func (pos sourcePosition) scriptOffset() int32 {
	if pos.isExternal() {
		return 0
	}
	return int32((pos >> 1) & ((1 << 30) - 1))
}

// decodePosition walks the given position table to find the value matching 'delta'.
// This maps a byte/native code 'delta' to a source code position (byte offset).
func decodePosition(table []byte, delta uint64) sourcePosition {
	// Logically the position table consists of lines with number pairs:
	// (code position, source position) and is sorted monotonically by the
	// code position. The actual stored zigzag value for both entries the
	// delta from previous entry.

	r := bytes.NewReader(table)
	//nolint:lll
	// Initialize implicit base values:
	// https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/codegen/source-position-table.h#26
	// https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/common/globals.h#480
	codePos := int64(-1)
	sourcePos := int64(0)
	for {
		codeDelta, err := decodeVLQ(r)
		if err != nil {
			if err == io.EOF {
				return sourcePosition(sourcePos)
			}
			return 1
		}
		if codeDelta >= 0 {
			codePos += codeDelta
		} else {
			codePos -= codeDelta + 1
		}

		// Stop when we find a code position greater than the one we are looking for
		if codePos > int64(delta) {
			return sourcePosition(sourcePos)
		}

		sourceDelta, err := decodeVLQ(r)
		if err != nil {
			return 1
		}
		sourcePos += sourceDelta
	}
}

// mapPositionToLine maps a file position (byte offset) to a line number. This is
// done against a table containing a offsets where each line ends.
func mapPositionToLine(lineEnds []uint32, pos int32) libpf.SourceLineno {
	if len(lineEnds) == 0 || pos < 0 {
		return 0
	}
	// Use binary search to locate the line number
	index := sort.Search(len(lineEnds), func(ndx int) bool {
		return lineEnds[ndx] >= uint32(pos)
	})
	return libpf.SourceLineno(index + 1)
}

// scriptOffsetToLine maps a sourcePosition to a line number in the corresponding source
func (sfi *v8SFI) scriptOffsetToLine(position sourcePosition) libpf.SourceLineno {
	scriptOffset := position.scriptOffset()
	// The scriptOffset is offset by one, to make kNoSourcePosition zero.
	//nolint:lll
	// https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/codegen/source-position.h#93
	if scriptOffset == 0 {
		return sfi.funcStartLine
	}
	return mapPositionToLine(sfi.source.lineTable, scriptOffset-1)
}

// appendFrame adds a new frame to frames.
func (i *v8Instance) appendFrame(frames *libpf.Frames, sfi *v8SFI, lineNo libpf.SourceLineno) {
	funcOffset := uint32(0)
	if lineNo > sfi.funcStartLine {
		funcOffset = uint32(lineNo - sfi.funcStartLine)
	}
	frames.Append(&libpf.Frame{
		Type:           libpf.V8Frame,
		FunctionName:   sfi.funcName,
		SourceFile:     sfi.source.fileName,
		SourceLine:     lineNo,
		FunctionOffset: funcOffset,
	})
}

var externalFunctionTag = libpf.Intern("<external-file>")

// generateNativeFrame and conditionally symbolizes a native frame.
func (i *v8Instance) generateNativeFrame(sourcePos sourcePosition, sfi *v8SFI,
	frames *libpf.Frames) {
	if sourcePos.isExternal() {
		frames.Append(&libpf.Frame{
			Type:         libpf.V8Frame,
			FunctionName: externalFunctionTag,
		})
		return
	}

	lineNo := sfi.scriptOffsetToLine(sourcePos)
	i.appendFrame(frames, sfi, lineNo)
}

// appendBytecodeFrame symbolizes and records to a trace a Bytecode based frame.
func (i *v8Instance) appendBytecodeFrame(sfi *v8SFI, delta uint64, frames *libpf.Frames) {
	sourcePos := decodePosition(sfi.bytecodePositionTable, delta)
	lineNo := sfi.scriptOffsetToLine(sourcePos)
	i.appendFrame(frames, sfi, lineNo)
}

// symbolizeSFI symbolizes and records to a trace a SharedFunctionInfo based frame.
func (i *v8Instance) symbolizeSFI(pointer libpf.Address, delta uint64, frames *libpf.Frames) error {
	vms := &i.d.vmStructs
	sfi, err := i.getSFI(pointer)
	if err != nil {
		return fmt.Errorf("getSFI: %w", err)
	}

	// Adjust the bytecode pointer as needed
	//nolint:lll
	// https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/execution/frames.cc#1793
	bytecodeDelta := int64(delta & support.V8LineDeltaMask)
	bytecodeDelta -= int64(vms.BytecodeArray.Data) - HeapObjectTag
	if bytecodeDelta < 0 {
		// Should not be happening
		bytecodeDelta = 0
	} else if bytecodeDelta >= int64(sfi.bytecodeLength) {
		// Invalid value
		bytecodeDelta = 0xffffffff
	}
	i.appendBytecodeFrame(sfi, uint64(bytecodeDelta), frames)
	return nil
}

// getBytecodeLength decodes the length at the start of bytecode array
func (d *v8Data) readBytecodeLength(r *bytes.Reader) int {
	// Bytecode has one optional scaling prefix opcode, followed with the meaningful opcode.
	//nolint:lll
	// The list of these opcodes is at:
	// https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/interpreter/bytecodes.h#46
	opcode, err := r.ReadByte()
	if err != nil {
		return 0
	}

	// The scaling opcodes have not changed since V8 6.8.141. So hard code them here.
	//nolint:lll
	// Map scaling opcodes to their offset in the decoding table:
	// https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/interpreter/bytecodes.h#612
	// https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/interpreter/bytecodes.h#892
	scaleOffset := uint(0)
	switch opcode {
	case 0x00, 0x02: // Wide, DebugBreakWide
		scaleOffset = uint(d.bytecodeCount)
	case 0x01, 0x03: // ExtraWide, DebugBreakExtraWide
		scaleOffset = 2 * uint(d.bytecodeCount)
	}

	prefixSize := 0
	if scaleOffset != 0 {
		prefixSize = 1
		opcode, err = r.ReadByte()
		if err != nil {
			return -1
		}
	}

	// Get the length from kBytecodeSizes
	//nolint:lll
	// https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/interpreter/bytecodes.h#893
	if opcode <= 0x03 && opcode > d.bytecodeCount {
		// Invalid opcode
		return -1
	}
	opcodeLength := int(d.bytecodeSizes[scaleOffset+uint(opcode)])
	if _, err := r.Seek(int64(opcodeLength)-1, io.SeekCurrent); err != nil {
		return -1
	}

	return prefixSize + opcodeLength
}

// mapBaselineCodeOffsetToBytecode decodes a Baseline PC offset into Bytecode offset
func (i *v8Instance) mapBaselineCodeOffsetToBytecode(code *v8Code, pcDelta uint32) uint32 {
	d := i.d
	if d.bytecodeSizes == nil || code.sfi.bytecode == nil || code.codePositionTable == nil {
		// Use the zero offset to report function start if needed data is not available.
		return 0
	}

	//nolint:lll
	// The algorithm to do the mapping:
	// https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/baseline/bytecode-offset-iterator.h#45
	// The baseline position table is a series of unsigned VLQs:
	// https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/baseline/bytecode-offset-iterator.h#78
	// https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/base/vlq.h#104
	pcReader := bytes.NewReader(code.codePositionTable)
	pcOffset := uint64(0)
	bytecodeReader := bytes.NewReader(code.sfi.bytecode)
	bytecodeOffset := uint32(0)

	for {
		pcLen, err := decodeUVLQ(pcReader)
		if err != nil {
			return 0
		}
		pcOffset += pcLen
		if pcOffset > uint64(pcDelta) {
			return bytecodeOffset
		}

		bytecodeLen := d.readBytecodeLength(bytecodeReader)
		if bytecodeLen <= 0 {
			// Invalid opcode, or end of bytecode
			return 0
		}
		bytecodeOffset += uint32(bytecodeLen)
	}
}

// symbolizeBaselineCode symbolizes and records to a trace a Baseline Code based frame.
func (i *v8Instance) symbolizeBaselineCode(code *v8Code, delta uint32, frames *libpf.Frames) {
	bytecodeDelta, ok := code.codeDeltaToPosition[delta]
	if !ok {
		// Decode bytecode delta and memoize it
		bytecodeDelta = sourcePosition(i.mapBaselineCodeOffsetToBytecode(code, delta))
		code.codeDeltaToPosition[delta] = bytecodeDelta
	}
	i.appendBytecodeFrame(code.sfi, uint64(bytecodeDelta), frames)
}

// symbolizeCode symbolizes and records to a trace a Code based frame.
func (i *v8Instance) symbolizeCode(code *v8Code, delta uint64, returnAddress bool,
	frames *libpf.Frames) error {
	var err error
	sfi := code.sfi
	delta &= support.V8LineDeltaMask

	// This is a native PC delta and points to the instruction after
	// the call function. Adjust to get the CALL instruction.
	if returnAddress && delta > 0 {
		delta--
	}

	if code.isBaseline {
		i.symbolizeBaselineCode(code, uint32(delta), frames)
		return nil
	}

	// Memoize the delta to position mapping to improve speed. We can't just
	// fully skip inlining handling as it may expand this frame to multiple ones.
	sourcePos, deltaSeen := code.codeDeltaToPosition[uint32(delta)]
	if !deltaSeen {
		sourcePos = decodePosition(code.codePositionTable, delta)
		code.codeDeltaToPosition[uint32(delta)] = sourcePos
	}

	for sourcePos.inliningID() != 0 {
		// struct SourcePosition {
		//   uint64_t value_;
		// };
		// struct InliningPosition {
		//   SourcePosition position = SourcePosition::Unknown();
		//   int inlined_function_id;
		// };

		sizeofInliningPosition := uint(16)
		inliningID := sourcePos.inliningID()
		itemOff := uint(inliningID-1) * sizeofInliningPosition
		funcID := npsr.Int32(code.inliningPositions, itemOff+8)
		inlinedSFI := sfi
		if funcID >= 0 {
			sfiPtr := npsr.Ptr(code.inliningSFIs, uint(funcID)*pointerSize)
			inlinedSFI, err = i.getSFI(sfiPtr)
			if err != nil {
				return fmt.Errorf("failed to get inlined SFI: %w", err)
			}
		}
		i.generateNativeFrame(sourcePos, inlinedSFI, frames)

		sourcePos = sourcePosition(npsr.Uint64(code.inliningPositions, itemOff))
		if sourcePos.inliningID() > inliningID {
			// This should not happen. The inliningIDs seen should be
			// monotonically decreasing.
			return fmt.Errorf("inlining ID is not monotonically decreasing (%v <= %v)",
				sourcePos.inliningID(), inliningID)
		}
	}
	i.generateNativeFrame(sourcePos, sfi, frames)

	return nil
}

func (i *v8Instance) Symbolize(frame *host.Frame, frames *libpf.Frames) error {
	if !frame.Type.IsInterpType(libpf.V8) {
		return interpreter.ErrMismatchInterpreterType
	}

	sfCounter := successfailurecounter.New(&i.successCount, &i.failCount)
	defer sfCounter.DefaultToFailure()

	pointerAndType := libpf.Address(frame.File)
	deltaOrMarker := uint64(frame.Lineno)
	frameType := pointerAndType & support.V8FileTypeMask
	pointer := pointerAndType&^support.V8FileTypeMask | HeapObjectTag

	var err error
	switch frameType {
	case support.V8FileTypeMarker:
		// This is a stub V8 frame, with deltaOrMarker containing the marker.
		// Convert the V8 build specific marker ID to a static ID and symbolize
		// that if needed.
		err = i.symbolizeMarkerFrame(deltaOrMarker, frames)
	case support.V8FileTypeByteCode, support.V8FileTypeNativeSFI:
		err = i.symbolizeSFI(pointer, deltaOrMarker, frames)
	case support.V8FileTypeNativeCode, support.V8FileTypeNativeJSFunc:
		var code *v8Code
		codeCookie := uint32(deltaOrMarker & support.V8LineCookieMask >> support.V8LineCookieShift)
		if frameType == support.V8FileTypeNativeCode {
			code, err = i.getCode(pointer, codeCookie)
		} else {
			code, err = i.getCodeFromJSFunc(pointer, codeCookie)
		}
		if err == nil {
			err = i.symbolizeCode(code, deltaOrMarker, frame.ReturnAddress, frames)
		}
	default:
		err = fmt.Errorf("unsupported frame type %#x", frameType)
	}
	if err != nil {
		// TODO: emit error frame
		return err
	}

	sfCounter.ReportSuccess()
	return nil
}

func (d *v8Data) String() string {
	ver := d.version
	return fmt.Sprintf("V8 %d.%d.%d", (ver>>24)&0xff, (ver>>16)&0xff, ver&0xffff)
}

// mapFramePointerOffset converts the frame pointer offset in bytes to eBPF used
// word offset relative to the number of slots read
func mapFramePointerOffset(relBytes uint8) uint8 {
	slotOffset := int(support.V8FpContextSize) + int(int8(relBytes))
	if slotOffset < 0 || slotOffset > support.V8FpContextSize-pointerSize {
		return support.V8FpContextSize
	}
	return uint8(slotOffset)
}

func (d *v8Data) Attach(ebpf interpreter.EbpfHandler, pid libpf.PID, _ libpf.Address,
	rm remotememory.RemoteMemory) (interpreter.Instance, error) {
	vms := &d.vmStructs
	data := support.V8ProcInfo{
		Version: d.version,

		Fp_marker:          mapFramePointerOffset(vms.FramePointer.Context),
		Fp_function:        mapFramePointerOffset(vms.FramePointer.Function),
		Fp_bytecode_offset: mapFramePointerOffset(vms.FramePointer.BytecodeOffset),

		Type_JSFunction_first:   vms.Fixed.FirstJSFunctionType,
		Type_JSFunction_last:    vms.Fixed.LastJSFunctionType,
		Type_Code:               vms.Type.Code,
		Type_SharedFunctionInfo: vms.Type.SharedFunctionInfo,

		Off_HeapObject_map:    uint8(vms.HeapObject.Map),
		Off_Map_instancetype:  uint8(vms.Map.InstanceType),
		Off_JSFunction_code:   uint8(vms.JSFunction.Code),
		Off_JSFunction_shared: uint8(vms.JSFunction.SharedFunctionInfo),

		Off_Code_instruction_start: uint8(vms.Code.InstructionStart),
		Off_Code_instruction_size:  uint8(vms.Code.InstructionSize),
		Off_Code_flags:             uint8(vms.Code.Flags),

		Codekind_shift:    vms.CodeKind.FieldShift,
		Codekind_mask:     uint8(vms.CodeKind.FieldMask),
		Codekind_baseline: vms.CodeKind.Baseline,
	}
	if err := ebpf.UpdateProcData(libpf.V8, pid, unsafe.Pointer(&data)); err != nil {
		return nil, err
	}

	addrToString, err := freelru.New[libpf.Address, libpf.String](
		interpreter.LruFunctionCacheSize, libpf.Address.Hash32)
	if err != nil {
		return nil, err
	}
	addrToCode, err := freelru.New[libpf.Address, *v8Code](interpreter.LruFunctionCacheSize,
		libpf.Address.Hash32)
	if err != nil {
		return nil, err
	}
	addrToSFI, err := freelru.New[libpf.Address, *v8SFI](interpreter.LruFunctionCacheSize,
		libpf.Address.Hash32)
	if err != nil {
		return nil, err
	}
	addrToSource, err := freelru.New[libpf.Address, *v8Source](lruSourceFileCacheSize,
		libpf.Address.Hash32)
	if err != nil {
		return nil, err
	}
	addrToType, err := freelru.New[libpf.Address, uint16](lruMapTypeCacheSize,
		libpf.Address.Hash32)
	if err != nil {
		return nil, err
	}

	return &v8Instance{
		d:            d,
		rm:           rm,
		mappings:     make(map[process.Mapping]*uint32),
		prefixes:     make(map[lpm.Prefix]*uint32),
		addrToString: addrToString,
		addrToCode:   addrToCode,
		addrToSFI:    addrToSFI,
		addrToSource: addrToSource,
		addrToType:   addrToType,
	}, nil
}

func (d *v8Data) Unload(_ interpreter.EbpfHandler) {
}

func (d *v8Data) readIntrospectionData(ef *pfelf.File) error {
	// Read the variables from the pfelf.File so we avoid failures if the process
	// exists during extraction of the introspection data.
	rm := ef.GetRemoteMemory()

	// Enumerate all symbols we are interested in
	vms := &d.vmStructs
	vmVal := reflect.ValueOf(vms).Elem()
	vmType := reflect.TypeOf(vms).Elem()
	for i := 0; i < vmVal.NumField(); i++ {
		classVal := vmVal.Field(i)
		classType := vmType.Field(i)
		className := classType.Name
		prefix := "v8dbg_"
		if nameTag, ok := classType.Tag.Lookup("name"); ok {
			className = nameTag
			if nameTag != "" {
				prefix += className + "_"
			}
		} else {
			prefix += "class_" + className + "__"
		}

		for j := 0; j < classVal.NumField(); j++ {
			memberType := classVal.Type().Field(j)
			memberVal := classVal.Field(j)

			memberName := memberType.Name
			if nameTag, ok := memberType.Tag.Lookup("name"); ok {
				memberName = nameTag
			}

			for _, n := range strings.Split(memberName, ",") {
				s := prefix + n
				if memberVal.Kind() == reflect.Bool {
					s = "v8dbg_parent_" + className + "__" + memberName
				}
				addr, err := ef.LookupSymbolAddress(libpf.SymbolName(s))
				if err != nil {
					log.Debugf("V8: %s = not found", s)
					if classType.Name == "FrameType" {
						memberVal.SetUint(^uint64(0))
					}
					continue
				}
				if memberVal.Kind() == reflect.Bool {
					log.Debugf("V8: %s exists", s)
					memberVal.SetBool(true)
				} else {
					val := rm.Uint32(libpf.Address(addr))
					log.Debugf("V8: %s = %#x", s, val)
					memberVal.SetUint(uint64(val))
				}
				break
			}
		}
	}

	// Add some defaults when needed
	if d.version >= v8Ver(11, 9, 0) {
		// the class hierarchy changed: HeapObject no longer
		// derives from Object. This confuses gen-postmortem-metadata.py
		// and it no longer emits class hierarchy information correctly.
		// But ScopeInfo indeed still derives from HeapObject, so just set
		// that manually here.
		vms.ScopeInfo.HeapObject = true
	}
	if d.version >= v8Ver(12, 5, 0) && !vms.DeoptimizationData.FixedArray &&
		!vms.DeoptimizationData.TrustedFixedArray {
		vms.DeoptimizationData.ProtectedFixedArray = true
	} else if d.version >= v8Ver(12, 3, 0) && !vms.DeoptimizationData.FixedArray {
		// DeoptimizationData changed base type to TrustedFixedArray, which doesn't have metadata.
		vms.DeoptimizationData.TrustedFixedArray = true
	}
	if d.version >= v8Ver(12, 4, 0) && !vms.SourcePositionTable.ByteArray {
		// SourcePositionTable changed base type to TrustedByteArray, which doesn't have metadata.
		vms.SourcePositionTable.TrustedByteArray = true
	}

	if vms.FramePointer.BytecodeArray == 0 {
		// Not available before V8 9.5.2
		if d.version >= v8Ver(8, 7, 198) {
			vms.FramePointer.BytecodeArray = vms.FramePointer.Function - 2*pointerSize
		} else {
			vms.FramePointer.BytecodeArray = vms.FramePointer.Function - 1*pointerSize
		}
	}
	if vms.FramePointer.BytecodeOffset == 0 {
		// Not available before V8 9.5.2
		vms.FramePointer.BytecodeOffset = vms.FramePointer.BytecodeArray - pointerSize
	}
	if vms.Fixed.FirstJSFunctionType == 0 {
		//nolint:lll
		// The V8 InstaceTypes tags are defined at:
		//   https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/objects/instance-type.h#124
		// which in turn is generated from the data at:
		//   https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/tools/v8heapconst.py#175
		// Since V8 9.0.14 the JSFunction is no longer a final class, but has several
		// classes inheriting form it. The only way to check for the inheritance is to
		// know which InstaceType tags belong to the range.
		numJSFuncTypes := uint16(1)
		switch {
		case d.version >= v8Ver(9, 6, 138):
			// Class constructor special case
			//nolint:lll
			// https://chromium.googlesource.com/v8/v8.git/+/1cd7a5822374a49ab6767185e69119d0d3076840
			numJSFuncTypes = 15
		case d.version >= v8Ver(9, 0, 14):
			// Several constructor special cases added
			//nolint:lll
			// https://chromium.googlesource.com/v8/v8.git/+/624030e975cb4384f877b65070b4e650a6acb1ef
			numJSFuncTypes = 14
		}
		vms.Fixed.FirstJSFunctionType = vms.Type.JSFunction
		vms.Fixed.LastJSFunctionType = vms.Fixed.FirstJSFunctionType + numJSFuncTypes - 1
	}
	if vms.JSFunction.Code == 0 {
		if d.version >= v8Ver(11, 7, 368) {
			vms.JSFunction.Code = vms.JSFunction.SharedFunctionInfo - pointerSize
		} else {
			// At least back to V8 8.4
			vms.JSFunction.Code = vms.JSFunction.SharedFunctionInfo + 3*pointerSize
		}
	}
	if vms.Code.InstructionSize != 0 {
		if vms.Code.SourcePositionTable == 0 {
			if vms.Type.CodeWrapper != 0 {
				// An extra pointer-sized variable for "code wrapper" was introduced
				// between `InstructionStart` and `PositionTable`, so the offset here becomes
				// 3 instead of 2.
				vms.Code.SourcePositionTable = vms.Code.InstructionStart - 3*pointerSize
			} else {
				// At least back to V8 8.4
				vms.Code.SourcePositionTable = vms.Code.InstructionSize - 2*pointerSize
			}
		}
		if vms.Code.Flags == 0 {
			// Back to V8 8.8.172
			vms.Code.Flags = vms.Code.InstructionSize + 2*4 // 2*sizeof(int)
		}
	} else if vms.Code.SourcePositionTable != 0 {
		// Likely V8 11.x where the Code postmortem data was accidentally deleted
		if vms.Code.DeoptimizationData == 0 {
			vms.Code.DeoptimizationData = vms.Code.SourcePositionTable - pointerSize
		}
		if vms.Code.InstructionStart == 0 {
			vms.Code.InstructionStart = vms.Code.SourcePositionTable + 2*pointerSize
		}
		if vms.Code.Flags == 0 {
			vms.Code.Flags = vms.Code.InstructionStart + pointerSize
		}
		if vms.Code.InstructionSize == 0 {
			vms.Code.InstructionSize = vms.Code.Flags + 4
			if d.version < v8Ver(11, 4, 59) {
				// V8 starting 11.1.x Code has kBuiltinIdOffset and kKindSpecificFlagsOffset
				// which changed again in 11.4.59 when these were removed in commit
				// cb8be519f0add9b7 "[code] Merge kind_specific_flags with flags"
				vms.Code.InstructionSize += 2 + 2
			}
		}
	}

	if vms.Code.DeoptimizationData == 0 && vms.Code.SourcePositionTable != 0 {
		// Used unconditionally, pending patch for V8 to export this
		// At least back to V8 7.2
		vms.Code.DeoptimizationData = vms.Code.SourcePositionTable - pointerSize
	}
	if vms.Script.Source == 0 {
		// At least back to V8 8.4
		vms.Script.Source = vms.Script.Name - pointerSize
	}
	if vms.BytecodeArray.SourcePositionTable == 0 {
		// Lost in V8 9.4
		vms.BytecodeArray.SourcePositionTable = vms.FixedArrayBase.Length + 3*pointerSize
	}
	if vms.BytecodeArray.Data == 0 {
		// At least back to V8 8.4 (16 = 3*int32 + uint16)
		vms.BytecodeArray.Data = vms.BytecodeArray.SourcePositionTable + pointerSize + 14
	}
	if vms.DeoptimizationDataIndex.InlinedFunctionCount == 0 {
		vms.DeoptimizationDataIndex.InlinedFunctionCount = 1
	}
	if vms.DeoptimizationDataIndex.LiteralArray == 0 {
		val := vms.DeoptimizationDataIndex.InlinedFunctionCount + 1
		vms.DeoptimizationDataIndex.LiteralArray = val
	}
	if vms.DeoptimizationDataIndex.SharedFunctionInfo == 0 &&
		vms.DeoptimizationDataIndex.SharedFunctionInfoWrapper == 0 {
		vms.DeoptimizationDataIndex.SharedFunctionInfo = 6
	}
	if vms.DeoptimizationDataIndex.InliningPositions == 0 {
		val := vms.DeoptimizationDataIndex.SharedFunctionInfo + 1
		vms.DeoptimizationDataIndex.InliningPositions = val
	}
	if vms.CodeKind.Baseline == 0 {
		if d.version >= v8Ver(9, 0, 240) {
			// Back to V8 9.0.240, and metadata available after that
			vms.CodeKind.FieldMask = 0xf
			vms.CodeKind.FieldShift = 0
			vms.CodeKind.Baseline = 11
		} else {
			// Leave mask and shift to zero, and set baseline to something
			// so that the Baseline code is never triggered.
			vms.CodeKind.Baseline = 0xff
		}
	}
	if vms.BaselineData.Data == 0 && vms.CodeKind.FieldMask != 0 {
		// Unfortunately no metadata currently. Has been static.
		vms.BaselineData.Data = vms.HeapObject.Map + 2*pointerSize
	}

	if vms.SharedFunctionInfo.FunctionData == 0 {
		// No metadata as of v8 242fa685d0c4eb07b27a167157e3b5c8cc70c244 --
		// note that RELEASE_ACQUIRE_ACCESSORS(SharedFunctionInfo, function_data, Tagged<Object>,
		//                          kFunctionDataOffset)
		// was removed from shared-function-info-inl.h .
		// Anyway, the way this works is changing with V8_SANDBOX,
		// but Node doesn't turn that on,
		// so we can probably get away with just hardcoding it for now.
		vms.SharedFunctionInfo.FunctionData = 8
	}
	if d.version >= v8Ver(12, 5, 0) {
		// This changed in f6c936e836b4d8ffafe790bcc3586f2ba5ffcf74
		vms.DeoptimizationLiteralArray.TrustedWeakFixedArray = true
	} else if d.version >= v8Ver(11, 9, 0) {
		// This had been WeakFixedArray for a very long time,
		// but we lost the metadata in 0698c376801dcde939850b7ad0b55c7459c83f4d.
		vms.DeoptimizationLiteralArray.WeakFixedArray = true
	}

	for i := 0; i < vmVal.NumField(); i++ {
		classVal := vmVal.Field(i)
		classType := vmType.Field(i)
		for j := 0; j < classVal.NumField(); j++ {
			memberType := classVal.Type().Field(j)
			memberVal := classVal.Field(j)
			log.Debugf("V8: %s::%s = %#x", classType.Name, memberType.Name, memberVal.Interface())
			if memberVal.Kind() == reflect.Bool || memberVal.Uint() != 0 {
				continue
			}
			if _, ok := memberType.Tag.Lookup("zero"); !ok {
				return fmt.Errorf("failed to get introspection data for %s.%s",
					classType.Name, memberType.Name)
			}
		}
	}

	return nil
}

func Loader(ebpf interpreter.EbpfHandler, info *interpreter.LoaderInfo) (interpreter.Data, error) {
	if !v8Regex.MatchString(info.FileName()) {
		return nil, nil
	}

	ef, err := info.GetELF()
	if err != nil {
		return nil, err
	}

	var vers [3]uint32
	for i, sym := range []string{"major", "minor", "build"} {
		// Resolve and read "v8::internal::Versions::XXXXXX_E"
		var val []byte
		sym = fmt.Sprintf("_ZN2v88internal7Version6%s_E", sym)
		_, val, err = ef.SymbolData(libpf.SymbolName(sym), 4)
		if err != nil {
			return nil, fmt.Errorf("unable to read '%s': %v", sym, err)
		}
		vers[i] = npsr.Uint32(val, 0)
	}

	version := vers[0]*0x1000000 + vers[1]*0x10000 + vers[2]
	log.Debugf("V8 version %v.%v.%v", vers[0], vers[1], vers[2])
	if vers[0] > 0xff || vers[1] > 0xff || vers[2] > 0xffff || version < 0x080100 {
		return nil, fmt.Errorf("version %v.%v.%v of V8 is not supported (minimum is 8.1.0)",
			vers[0], vers[1], vers[2])
	}

	d := &v8Data{
		version: version,
	}

	addr, err := ef.LookupSymbolAddress("_ZN2v88internal8Snapshot19DefaultSnapshotBlobEv")
	if err == nil {
		// If there is a big stack delta soon after v8::internal::Snapshot::DefaultSnapshotBlob()
		// assume it is the V8 snapshot data.
		for _, gap := range info.Gaps() {
			if gap.Start-uint64(addr) < 1024 {
				d.snapshotRange = gap
				log.Debugf("V8 JIT Area: %#v", d.snapshotRange)
				break
			}
		}
	}

	sym, err := ef.LookupSymbol("_ZN2v88internal11interpreter9Bytecodes14kBytecodeSizesE")
	if err == nil && sym.Size%3 == 0 && sym.Size < 3*256 {
		// Symbol v8::internal::interpreter::Bytecodes::kBytecodeSizes:
		// static const uint8_t Bytecodes::kBytecodeSizes[3][kBytecodeCount];
		log.Debugf("V8: bytecode sizes at %x, length %d, %d opcodes",
			sym.Address, sym.Size, sym.Size/3)
		d.bytecodeSizes = make([]byte, sym.Size)
		d.bytecodeCount = uint8(sym.Size / 3)
		if _, err = ef.ReadVirtualMemory(d.bytecodeSizes, int64(sym.Address)); err != nil {
			return nil, fmt.Errorf("unable to read bytecode sizes: %v", err)
		}
		for _, opcodeLength := range d.bytecodeSizes {
			// Check for valid opcode size. Largest seen so far is 17 bytes.
			if opcodeLength <= 0 || opcodeLength >= 32 {
				log.Debugf("V8: invalid bytecode opcode size: %d", opcodeLength)
				d.bytecodeSizes = nil
				d.bytecodeCount = 0
				break
			}
		}
	}

	// load introspection data
	if err = d.readIntrospectionData(ef); err != nil {
		return nil, err
	}

	vms := &d.vmStructs
	if vms.Fixed.HeapObjectTag != HeapObjectTag ||
		vms.Fixed.HeapObjectTagMask != HeapObjectTagMask ||
		vms.Fixed.SmiTag != SmiTag || vms.Fixed.SmiTagMask != SmiTagMask ||
		vms.Fixed.SmiShiftSize != SmiValueShift-SmiTagShift {
		return nil, errors.New("incompatible tagging scheme")
	}

	if mapFramePointerOffset(vms.FramePointer.Context) >= support.V8FpContextSize ||
		mapFramePointerOffset(vms.FramePointer.Function) >= support.V8FpContextSize ||
		mapFramePointerOffset(vms.FramePointer.BytecodeOffset) >= support.V8FpContextSize {
		return nil, fmt.Errorf("incompatible framepointer offsets (%d/%d/%d)",
			vms.FramePointer.Context, vms.FramePointer.Function,
			vms.FramePointer.BytecodeOffset)
	}

	if d.snapshotRange.Start != 0 {
		if err = ebpf.UpdateInterpreterOffsets(support.ProgUnwindV8, info.FileID(),
			[]util.Range{d.snapshotRange}); err != nil {
			return nil, err
		}
	}

	return d, nil
}
