Reland "[vm] Fix V8 snapshot profile handling of the dispatch table."
This is a reland of 5909fd111d
Does a large refactoring on the V8 snapshot profile writer
to clean things up, add more debugging support, and to fix
the problems that surfaced during the original landing.
Other changes:
Changes Serializer::CreateArtificialNodeIfNeeded() to create
artificial nodes for Code objects and immutable arrays.
Fixes CodeSerializationCluster::Trace() to only push needed parts of
discarded code objects, instead of tracing them like full code objects.
Adds test cases to v8_snapshot_profile_writer_test that exercise
the following situations (both separately and together):
* Non-symbolic stack traces are enabled and code and function objects
are dropped when not needed at runtime.
* Creation of the dispatch table is disabled.
TEST=vm/dart{,_2}/v8_snapshot_profile_writer_test
Original change's description:
> [vm] Fix V8 snapshot profile handling of the dispatch table.
>
> Fixes https://github.com/dart-lang/sdk/issues/45702.
>
> TEST=Tests listed in the issue above.
>
> Cq-Include-Trybots: luci.dart.try:vm-kernel-precomp-linux-debug-x64-try
> Change-Id: Ibf5e3ccf3828c01f9dda47de360314dabe8cb8a9
> Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/195272
> Reviewed-by: Daco Harkes <dacoharkes@google.com>
> Commit-Queue: Tess Strickland <sstrickl@google.com>
Change-Id: I8e7030267fe190079a8f68d00fe20bf7170e5719
Cq-Include-Trybots: luci.dart.try:vm-kernel-precomp-linux-debug-x64-try,vm-kernel-precomp-linux-product-x64-try,vm-kernel-precomp-mac-release-simarm64-try,vm-kernel-precomp-linux-debug-x64c-try,vm-kernel-precomp-obfuscate-linux-release-x64-try
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/195513
Reviewed-by: Daco Harkes <dacoharkes@google.com>
Commit-Queue: Tess Strickland <sstrickl@google.com>
This commit is contained in:
committed by
commit-bot@chromium.org
parent
69d56fd82a
commit
6c8f4aa190
@@ -168,6 +168,15 @@ class Expect {
|
||||
_fail("Expect.isNotNull(actual: <$actual>$msg) fails.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the Iterable [actual] is empty.
|
||||
*/
|
||||
static void isEmpty(Iterable actual, [String reason = ""]) {
|
||||
if (actual.isEmpty) return;
|
||||
String msg = _getMessage(reason);
|
||||
_fail("Expect.isEmpty(actual: <$actual>$msg) fails.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the expected and actual values are identical
|
||||
* (using `identical`).
|
||||
|
||||
@@ -50,35 +50,6 @@ int Utils::CountOneBits32(uint32_t x) {
|
||||
#endif
|
||||
}
|
||||
|
||||
// TODO(koda): Compare to flsll call/intrinsic.
|
||||
int Utils::HighestBit(int64_t v) {
|
||||
uint64_t x = static_cast<uint64_t>((v > 0) ? v : -v);
|
||||
uint64_t t;
|
||||
int r = 0;
|
||||
if ((t = x >> 32) != 0) {
|
||||
x = t;
|
||||
r += 32;
|
||||
}
|
||||
if ((t = x >> 16) != 0) {
|
||||
x = t;
|
||||
r += 16;
|
||||
}
|
||||
if ((t = x >> 8) != 0) {
|
||||
x = t;
|
||||
r += 8;
|
||||
}
|
||||
if ((t = x >> 4) != 0) {
|
||||
x = t;
|
||||
r += 4;
|
||||
}
|
||||
if ((t = x >> 2) != 0) {
|
||||
x = t;
|
||||
r += 2;
|
||||
}
|
||||
if (x > 1) r += 1;
|
||||
return r;
|
||||
}
|
||||
|
||||
int Utils::CountLeadingZeros64(uint64_t x) {
|
||||
#if defined(ARCH_IS_32_BIT)
|
||||
const uint32_t x_hi = static_cast<uint32_t>(x >> 32);
|
||||
|
||||
@@ -117,9 +117,36 @@ class Utils {
|
||||
#endif
|
||||
}
|
||||
|
||||
static int HighestBit(int64_t v);
|
||||
// TODO(koda): Compare to flsll call/intrinsic.
|
||||
static constexpr size_t HighestBit(int64_t v) {
|
||||
uint64_t x = static_cast<uint64_t>((v > 0) ? v : -v);
|
||||
uint64_t t = 0;
|
||||
size_t r = 0;
|
||||
if ((t = x >> 32) != 0) {
|
||||
x = t;
|
||||
r += 32;
|
||||
}
|
||||
if ((t = x >> 16) != 0) {
|
||||
x = t;
|
||||
r += 16;
|
||||
}
|
||||
if ((t = x >> 8) != 0) {
|
||||
x = t;
|
||||
r += 8;
|
||||
}
|
||||
if ((t = x >> 4) != 0) {
|
||||
x = t;
|
||||
r += 4;
|
||||
}
|
||||
if ((t = x >> 2) != 0) {
|
||||
x = t;
|
||||
r += 2;
|
||||
}
|
||||
if (x > 1) r += 1;
|
||||
return r;
|
||||
}
|
||||
|
||||
static int BitLength(int64_t value) {
|
||||
static constexpr size_t BitLength(int64_t value) {
|
||||
// Flip bits if negative (-1 becomes 0).
|
||||
value ^= value >> (8 * sizeof(value) - 1);
|
||||
return (value == 0) ? 0 : (Utils::HighestBit(value) + 1);
|
||||
|
||||
@@ -9,6 +9,8 @@ import "dart:convert";
|
||||
import 'package:expect/expect.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
import 'use_flag_test_helper.dart';
|
||||
|
||||
main(List<String> args) async {
|
||||
if (!Platform.executable.endsWith("dart_precompiled_runtime")) {
|
||||
return; // Running in JIT: AOT binaries not available.
|
||||
@@ -24,7 +26,7 @@ main(List<String> args) async {
|
||||
final genSnapshot = path.join(buildDir, 'gen_snapshot');
|
||||
final aotRuntime = path.join(buildDir, 'dart_precompiled_runtime');
|
||||
|
||||
await withTempDir((String tempDir) async {
|
||||
await withTempDir('emit_aot_size_info_flag', (String tempDir) async {
|
||||
final script = path.join(sdkDir, 'pkg/kernel/bin/dump.dart');
|
||||
final scriptDill = path.join(tempDir, 'kernel_dump.dill');
|
||||
final appHeapsnapshot = path.join(tempDir, 'app.heapsnapshot');
|
||||
@@ -77,33 +79,3 @@ main(List<String> args) async {
|
||||
Future<String> readFile(String file) {
|
||||
return new File(file).readAsString();
|
||||
}
|
||||
|
||||
Future run(String executable, List<String> args) async {
|
||||
print('Running $executable ${args.join(' ')}');
|
||||
|
||||
final result = await Process.run(executable, args);
|
||||
final String stdout = result.stdout;
|
||||
final String stderr = result.stderr;
|
||||
if (stdout.isNotEmpty) {
|
||||
print('stdout:');
|
||||
print(stdout);
|
||||
}
|
||||
if (stderr.isNotEmpty) {
|
||||
print('stderr:');
|
||||
print(stderr);
|
||||
}
|
||||
|
||||
if (result.exitCode != 0) {
|
||||
throw 'Command failed with non-zero exit code (was ${result.exitCode})';
|
||||
}
|
||||
}
|
||||
|
||||
Future withTempDir(Future fun(String dir)) async {
|
||||
final tempDir =
|
||||
Directory.systemTemp.createTempSync('aot-size-info-flags-test');
|
||||
try {
|
||||
await fun(tempDir.path);
|
||||
} finally {
|
||||
tempDir.deleteSync(recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,34 +11,61 @@ import 'package:vm_snapshot_analysis/v8_profile.dart';
|
||||
|
||||
import 'use_flag_test_helper.dart';
|
||||
|
||||
test(
|
||||
{required String dillPath,
|
||||
required bool useAsm,
|
||||
required bool useBare,
|
||||
required bool stripFlag,
|
||||
required bool stripUtil,
|
||||
// Used to ensure we don't have multiple equivalent calls to test.
|
||||
final _seenDescriptions = <String>{};
|
||||
|
||||
Future<void> test(String dillPath,
|
||||
{bool useAsm = false,
|
||||
bool useBare = true,
|
||||
bool forceDrops = false,
|
||||
bool useDispatch = true,
|
||||
bool stripUtil = false, // Note: forced if useAsm.
|
||||
bool stripFlag = false, // Note: forced if !stripUtil (and thus !useAsm).
|
||||
bool disassemble = false}) async {
|
||||
// We don't assume forced disassembler support in Product mode, so skip any
|
||||
// disassembly test.
|
||||
if (!const bool.fromEnvironment('dart.vm.product') && disassemble) return;
|
||||
if (!const bool.fromEnvironment('dart.vm.product') && disassemble) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The assembler may add extra unnecessary information to the compiled
|
||||
// snapshot whether or not we generate DWARF information in the assembly, so
|
||||
// we force the use of a utility when generating assembly.
|
||||
if (useAsm) Expect.isTrue(stripUtil);
|
||||
if (useAsm) {
|
||||
stripUtil = true;
|
||||
}
|
||||
|
||||
// We must strip the output in some way when generating ELF snapshots,
|
||||
// else the debugging information added will cause the test to fail.
|
||||
if (!stripUtil) Expect.isTrue(stripFlag);
|
||||
if (!stripUtil) {
|
||||
stripFlag = true;
|
||||
}
|
||||
|
||||
final tempDirPrefix = 'v8-snapshot-profile' +
|
||||
(useAsm ? '-assembly' : '-elf') +
|
||||
(useBare ? '-bare' : '-nonbare') +
|
||||
(stripFlag ? '-intstrip' : '') +
|
||||
(stripUtil ? '-extstrip' : '') +
|
||||
(disassemble ? '-disassembled' : '');
|
||||
final descriptionBuilder = StringBuffer()..write(useAsm ? 'assembly' : 'elf');
|
||||
if (!useBare) {
|
||||
descriptionBuilder.write('-nonbare');
|
||||
}
|
||||
if (forceDrops) {
|
||||
descriptionBuilder.write('-dropped');
|
||||
}
|
||||
if (!useDispatch) {
|
||||
descriptionBuilder.write('-nodispatch');
|
||||
}
|
||||
if (stripFlag) {
|
||||
descriptionBuilder.write('-intstrip');
|
||||
}
|
||||
if (stripUtil) {
|
||||
descriptionBuilder.write('-extstrip');
|
||||
}
|
||||
if (disassemble) {
|
||||
descriptionBuilder.write('-disassembled');
|
||||
}
|
||||
|
||||
await withTempDir(tempDirPrefix, (String tempDir) async {
|
||||
final description = descriptionBuilder.toString();
|
||||
Expect.isTrue(_seenDescriptions.add(description),
|
||||
"test configuration $description would be run multiple times");
|
||||
|
||||
await withTempDir('v8-snapshot-profile-$description', (String tempDir) async {
|
||||
// Generate the snapshot profile.
|
||||
final profilePath = path.join(tempDir, 'profile.heapsnapshot');
|
||||
final snapshotPath = path.join(tempDir, 'test.snap');
|
||||
@@ -46,6 +73,12 @@ test(
|
||||
if (stripFlag) '--strip',
|
||||
useBare ? '--use-bare-instructions' : '--no-use-bare-instructions',
|
||||
"--write-v8-snapshot-profile-to=$profilePath",
|
||||
if (forceDrops) ...[
|
||||
'--dwarf-stack-traces',
|
||||
'--no-retain-function-objects',
|
||||
'--no-retain-code-objects'
|
||||
],
|
||||
if (!useDispatch) '--no-use-table-dispatch',
|
||||
if (disassemble) '--disassemble',
|
||||
'--ignore-unrecognized-flags',
|
||||
dillPath,
|
||||
@@ -77,6 +110,8 @@ test(
|
||||
strippedPath = snapshotPath;
|
||||
}
|
||||
|
||||
print("Snapshot profile generated at $profilePath.");
|
||||
|
||||
final profile =
|
||||
Snapshot.fromJson(jsonDecode(File(profilePath).readAsStringSync()));
|
||||
|
||||
@@ -84,39 +119,38 @@ test(
|
||||
// reference to an some object but no other metadata about the object was
|
||||
// recorded. We should at least record the type for every object in the
|
||||
// graph (in some cases the shallow size can legitimately be 0, e.g. for
|
||||
// "base objects").
|
||||
// "base objects" not written to the snapshot or artificial nodes).
|
||||
for (final node in profile.nodes) {
|
||||
Expect.notEquals("Unknown", node.type, "unknown node at ID ${node.id}");
|
||||
Expect.notEquals("Unknown", node.type, "unknown node ${node}");
|
||||
}
|
||||
|
||||
final root = profile.nodeAt(0);
|
||||
final reachable = <Node>{};
|
||||
|
||||
// HeapSnapshotWorker.HeapSnapshot.calculateDistances (from HeapSnapshot.js)
|
||||
// assumes that the root does not have more than one edge to any other node
|
||||
// assumes that the graph root has at most one edge to any other node
|
||||
// (most likely an oversight).
|
||||
final Set<int> roots = <int>{};
|
||||
for (final edge in profile.nodeAt(0).edges) {
|
||||
Expect.isTrue(roots.add(edge.target.index));
|
||||
for (final edge in root.edges) {
|
||||
Expect.isTrue(
|
||||
reachable.add(edge.target),
|
||||
"root\n\n$root\n\nhas multiple edges to node\n\n${edge.target}:\n\n"
|
||||
"${root.edges.where((e) => e.target == edge.target).toList()}");
|
||||
}
|
||||
|
||||
// Check that all nodes are reachable from the root (index 0).
|
||||
final Set<int> reachable = {0};
|
||||
final dfs = <int>[0];
|
||||
while (!dfs.isEmpty) {
|
||||
final next = dfs.removeLast();
|
||||
for (final edge in profile.nodeAt(next).edges) {
|
||||
final target = edge.target;
|
||||
if (!reachable.contains(target.index)) {
|
||||
reachable.add(target.index);
|
||||
dfs.add(target.index);
|
||||
// Check that all other nodes are reachable from the root.
|
||||
final stack = <Node>[...reachable];
|
||||
while (!stack.isEmpty) {
|
||||
final next = stack.removeLast();
|
||||
for (final edge in next.edges) {
|
||||
if (reachable.add(edge.target)) {
|
||||
stack.add(edge.target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (reachable.length != profile.nodeCount) {
|
||||
for (final node in profile.nodes) {
|
||||
Expect.isTrue(reachable.contains(node.index),
|
||||
"unreachable node at ID ${node.id}");
|
||||
}
|
||||
}
|
||||
final unreachable =
|
||||
profile.nodes.skip(1).where((Node n) => !reachable.contains(n)).toSet();
|
||||
Expect.isEmpty(unreachable);
|
||||
|
||||
// Verify that the actual size of the snapshot is close to the sum of the
|
||||
// shallow sizes of all objects in the profile. They will not be exactly
|
||||
@@ -124,25 +158,14 @@ test(
|
||||
final actual = await File(strippedPath).length();
|
||||
final expected = profile.nodes.fold<int>(0, (size, n) => size + n.selfSize);
|
||||
|
||||
final bareUsed = useBare ? "bare" : "non-bare";
|
||||
final fileType = useAsm ? "assembly" : "ELF";
|
||||
String stripPrefix = "";
|
||||
if (stripFlag && stripUtil) {
|
||||
stripPrefix = "internally and externally stripped ";
|
||||
} else if (stripFlag) {
|
||||
stripPrefix = "internally stripped ";
|
||||
} else if (stripUtil) {
|
||||
stripPrefix = "externally stripped ";
|
||||
}
|
||||
|
||||
// See Elf::kPages in runtime/vm/elf.h.
|
||||
final segmentAlignment = 16384;
|
||||
final segmentAlignment = 16 * 1024;
|
||||
// Not every byte is accounted for by the snapshot profile, and data and
|
||||
// instruction segments are padded to an alignment boundary.
|
||||
final tolerance = 0.03 * actual + 2 * segmentAlignment;
|
||||
|
||||
Expect.approxEquals(expected, actual, tolerance,
|
||||
"failed on $bareUsed $stripPrefix$fileType snapshot type.");
|
||||
Expect.approxEquals(
|
||||
expected, actual, tolerance, "failed on $description snapshot");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -258,28 +281,22 @@ main() async {
|
||||
_thisTestPath
|
||||
]);
|
||||
|
||||
// Just as a reminder (these rules are applied in order inside test):
|
||||
// If useAsm is true, then stripUtil is forced (as the assembler may add
|
||||
// extra information that needs stripping).
|
||||
// If stripUtil is false, then stripFlag is forced (as the output must be
|
||||
// stripped in some way to remove DWARF information).
|
||||
|
||||
// Test stripped ELF generation directly.
|
||||
await test(
|
||||
dillPath: dillPath,
|
||||
stripFlag: true,
|
||||
stripUtil: false,
|
||||
useAsm: false,
|
||||
useBare: false);
|
||||
await test(
|
||||
dillPath: dillPath,
|
||||
stripFlag: true,
|
||||
stripUtil: false,
|
||||
useAsm: false,
|
||||
useBare: true);
|
||||
await test(dillPath);
|
||||
await test(dillPath, useBare: false);
|
||||
await test(dillPath, forceDrops: true);
|
||||
await test(dillPath, forceDrops: true, useBare: false);
|
||||
await test(dillPath, forceDrops: true, useDispatch: false);
|
||||
await test(dillPath, forceDrops: true, useDispatch: false, useBare: false);
|
||||
|
||||
// Regression test for dartbug.com/41149.
|
||||
await test(
|
||||
dillPath: dillPath,
|
||||
stripFlag: true,
|
||||
stripUtil: false,
|
||||
useAsm: false,
|
||||
useBare: false,
|
||||
disassemble: true);
|
||||
await test(dillPath, useBare: false, disassemble: true);
|
||||
|
||||
// We neither generate assembly nor have a stripping utility on Windows.
|
||||
if (Platform.isWindows) {
|
||||
@@ -292,18 +309,8 @@ main() async {
|
||||
printSkip('ELF external stripping test');
|
||||
} else {
|
||||
// Test unstripped ELF generation that is then stripped externally.
|
||||
await test(
|
||||
dillPath: dillPath,
|
||||
stripFlag: false,
|
||||
stripUtil: true,
|
||||
useAsm: false,
|
||||
useBare: false);
|
||||
await test(
|
||||
dillPath: dillPath,
|
||||
stripFlag: false,
|
||||
stripUtil: true,
|
||||
useAsm: false,
|
||||
useBare: true);
|
||||
await test(dillPath, stripUtil: true);
|
||||
await test(dillPath, stripUtil: true, useBare: false);
|
||||
}
|
||||
|
||||
// TODO(sstrickl): Currently we can't assemble for SIMARM64 on MacOSX.
|
||||
@@ -314,31 +321,11 @@ main() async {
|
||||
return;
|
||||
}
|
||||
|
||||
// Test stripped assembly generation that is then compiled and stripped.
|
||||
await test(
|
||||
dillPath: dillPath,
|
||||
stripFlag: true,
|
||||
stripUtil: true,
|
||||
useAsm: true,
|
||||
useBare: false);
|
||||
await test(
|
||||
dillPath: dillPath,
|
||||
stripFlag: true,
|
||||
stripUtil: true,
|
||||
useAsm: true,
|
||||
useBare: true);
|
||||
// Test unstripped assembly generation that is then compiled and stripped.
|
||||
await test(
|
||||
dillPath: dillPath,
|
||||
stripFlag: false,
|
||||
stripUtil: true,
|
||||
useAsm: true,
|
||||
useBare: false);
|
||||
await test(
|
||||
dillPath: dillPath,
|
||||
stripFlag: false,
|
||||
stripUtil: true,
|
||||
useAsm: true,
|
||||
useBare: true);
|
||||
await test(dillPath, useAsm: true);
|
||||
await test(dillPath, useAsm: true, useBare: false);
|
||||
// Test stripped assembly generation that is then compiled and stripped.
|
||||
await test(dillPath, useAsm: true, stripFlag: true);
|
||||
await test(dillPath, useAsm: true, stripFlag: true, useBare: false);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ import "dart:convert";
|
||||
import 'package:expect/expect.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
import 'use_flag_test_helper.dart';
|
||||
|
||||
main(List<String> args) async {
|
||||
if (!Platform.executable.endsWith("dart_precompiled_runtime")) {
|
||||
return; // Running in JIT: AOT binaries not available.
|
||||
@@ -24,7 +26,7 @@ main(List<String> args) async {
|
||||
final genSnapshot = path.join(buildDir, 'gen_snapshot');
|
||||
final aotRuntime = path.join(buildDir, 'dart_precompiled_runtime');
|
||||
|
||||
await withTempDir((String tempDir) async {
|
||||
await withTempDir('emit_aot_size_info_flag', (String tempDir) async {
|
||||
final script = path.join(sdkDir, 'pkg/kernel/bin/dump.dart');
|
||||
final scriptDill = path.join(tempDir, 'kernel_dump.dill');
|
||||
final appHeapsnapshot = path.join(tempDir, 'app.heapsnapshot');
|
||||
@@ -77,33 +79,3 @@ main(List<String> args) async {
|
||||
Future<String> readFile(String file) {
|
||||
return new File(file).readAsString();
|
||||
}
|
||||
|
||||
Future run(String executable, List<String> args) async {
|
||||
print('Running $executable ${args.join(' ')}');
|
||||
|
||||
final result = await Process.run(executable, args);
|
||||
final String stdout = result.stdout;
|
||||
final String stderr = result.stderr;
|
||||
if (stdout.isNotEmpty) {
|
||||
print('stdout:');
|
||||
print(stdout);
|
||||
}
|
||||
if (stderr.isNotEmpty) {
|
||||
print('stderr:');
|
||||
print(stderr);
|
||||
}
|
||||
|
||||
if (result.exitCode != 0) {
|
||||
throw 'Command failed with non-zero exit code (was ${result.exitCode})';
|
||||
}
|
||||
}
|
||||
|
||||
Future withTempDir(Future fun(String dir)) async {
|
||||
final tempDir =
|
||||
Directory.systemTemp.createTempSync('aot-size-info-flags-test');
|
||||
try {
|
||||
await fun(tempDir.path);
|
||||
} finally {
|
||||
tempDir.deleteSync(recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,34 +11,61 @@ import 'package:vm_snapshot_analysis/v8_profile.dart';
|
||||
|
||||
import 'use_flag_test_helper.dart';
|
||||
|
||||
test(
|
||||
{String dillPath,
|
||||
bool useAsm,
|
||||
bool useBare,
|
||||
bool stripFlag,
|
||||
bool stripUtil,
|
||||
// Used to ensure we don't have multiple equivalent calls to test.
|
||||
final _seenDescriptions = <String>{};
|
||||
|
||||
Future<void> test(String dillPath,
|
||||
{bool useAsm = false,
|
||||
bool useBare = true,
|
||||
bool forceDrops = false,
|
||||
bool useDispatch = true,
|
||||
bool stripUtil = false, // Note: forced if useAsm.
|
||||
bool stripFlag = false, // Note: forced if !stripUtil (and thus !useAsm).
|
||||
bool disassemble = false}) async {
|
||||
// We don't assume forced disassembler support in Product mode, so skip any
|
||||
// disassembly test.
|
||||
if (!const bool.fromEnvironment('dart.vm.product') && disassemble) return;
|
||||
if (!const bool.fromEnvironment('dart.vm.product') && disassemble) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The assembler may add extra unnecessary information to the compiled
|
||||
// snapshot whether or not we generate DWARF information in the assembly, so
|
||||
// we force the use of a utility when generating assembly.
|
||||
if (useAsm) Expect.isTrue(stripUtil);
|
||||
if (useAsm) {
|
||||
stripUtil = true;
|
||||
}
|
||||
|
||||
// We must strip the output in some way when generating ELF snapshots,
|
||||
// else the debugging information added will cause the test to fail.
|
||||
if (!stripUtil) Expect.isTrue(stripFlag);
|
||||
if (!stripUtil) {
|
||||
stripFlag = true;
|
||||
}
|
||||
|
||||
final tempDirPrefix = 'v8-snapshot-profile' +
|
||||
(useAsm ? '-assembly' : '-elf') +
|
||||
(useBare ? '-bare' : '-nonbare') +
|
||||
(stripFlag ? '-intstrip' : '') +
|
||||
(stripUtil ? '-extstrip' : '') +
|
||||
(disassemble ? '-disassembled' : '');
|
||||
final descriptionBuilder = StringBuffer()..write(useAsm ? 'assembly' : 'elf');
|
||||
if (!useBare) {
|
||||
descriptionBuilder.write('-nonbare');
|
||||
}
|
||||
if (forceDrops) {
|
||||
descriptionBuilder.write('-dropped');
|
||||
}
|
||||
if (!useDispatch) {
|
||||
descriptionBuilder.write('-nodispatch');
|
||||
}
|
||||
if (stripFlag) {
|
||||
descriptionBuilder.write('-intstrip');
|
||||
}
|
||||
if (stripUtil) {
|
||||
descriptionBuilder.write('-extstrip');
|
||||
}
|
||||
if (disassemble) {
|
||||
descriptionBuilder.write('-disassembled');
|
||||
}
|
||||
|
||||
await withTempDir(tempDirPrefix, (String tempDir) async {
|
||||
final description = descriptionBuilder.toString();
|
||||
Expect.isTrue(_seenDescriptions.add(description),
|
||||
"test configuration $description would be run multiple times");
|
||||
|
||||
await withTempDir('v8-snapshot-profile-$description', (String tempDir) async {
|
||||
// Generate the snapshot profile.
|
||||
final profilePath = path.join(tempDir, 'profile.heapsnapshot');
|
||||
final snapshotPath = path.join(tempDir, 'test.snap');
|
||||
@@ -46,6 +73,12 @@ test(
|
||||
if (stripFlag) '--strip',
|
||||
useBare ? '--use-bare-instructions' : '--no-use-bare-instructions',
|
||||
"--write-v8-snapshot-profile-to=$profilePath",
|
||||
if (forceDrops) ...[
|
||||
'--dwarf-stack-traces',
|
||||
'--no-retain-function-objects',
|
||||
'--no-retain-code-objects'
|
||||
],
|
||||
if (!useDispatch) '--no-use-table-dispatch',
|
||||
if (disassemble) '--disassemble',
|
||||
'--ignore-unrecognized-flags',
|
||||
dillPath,
|
||||
@@ -77,6 +110,8 @@ test(
|
||||
strippedPath = snapshotPath;
|
||||
}
|
||||
|
||||
print("Snapshot profile generated at $profilePath.");
|
||||
|
||||
final profile =
|
||||
Snapshot.fromJson(jsonDecode(File(profilePath).readAsStringSync()));
|
||||
|
||||
@@ -84,39 +119,38 @@ test(
|
||||
// reference to an some object but no other metadata about the object was
|
||||
// recorded. We should at least record the type for every object in the
|
||||
// graph (in some cases the shallow size can legitimately be 0, e.g. for
|
||||
// "base objects").
|
||||
// "base objects" not written to the snapshot or artificial nodes).
|
||||
for (final node in profile.nodes) {
|
||||
Expect.notEquals("Unknown", node.type, "unknown node at ID ${node.id}");
|
||||
Expect.notEquals("Unknown", node.type, "unknown node ${node}");
|
||||
}
|
||||
|
||||
final root = profile.nodeAt(0);
|
||||
final reachable = <Node>{};
|
||||
|
||||
// HeapSnapshotWorker.HeapSnapshot.calculateDistances (from HeapSnapshot.js)
|
||||
// assumes that the root does not have more than one edge to any other node
|
||||
// assumes that the graph root has at most one edge to any other node
|
||||
// (most likely an oversight).
|
||||
final Set<int> roots = <int>{};
|
||||
for (final edge in profile.nodeAt(0).edges) {
|
||||
Expect.isTrue(roots.add(edge.target.index));
|
||||
for (final edge in root.edges) {
|
||||
Expect.isTrue(
|
||||
reachable.add(edge.target),
|
||||
"root\n\n$root\n\nhas multiple edges to node\n\n${edge.target}:\n\n"
|
||||
"${root.edges.where((e) => e.target == edge.target).toList()}");
|
||||
}
|
||||
|
||||
// Check that all nodes are reachable from the root (index 0).
|
||||
final Set<int> reachable = {0};
|
||||
final dfs = <int>[0];
|
||||
while (!dfs.isEmpty) {
|
||||
final next = dfs.removeLast();
|
||||
for (final edge in profile.nodeAt(next).edges) {
|
||||
final target = edge.target;
|
||||
if (!reachable.contains(target.index)) {
|
||||
reachable.add(target.index);
|
||||
dfs.add(target.index);
|
||||
// Check that all other nodes are reachable from the root.
|
||||
final stack = <Node>[...reachable];
|
||||
while (!stack.isEmpty) {
|
||||
final next = stack.removeLast();
|
||||
for (final edge in next.edges) {
|
||||
if (reachable.add(edge.target)) {
|
||||
stack.add(edge.target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (reachable.length != profile.nodeCount) {
|
||||
for (final node in profile.nodes) {
|
||||
Expect.isTrue(reachable.contains(node.index),
|
||||
"unreachable node at ID ${node.id}");
|
||||
}
|
||||
}
|
||||
final unreachable =
|
||||
profile.nodes.skip(1).where((Node n) => !reachable.contains(n)).toSet();
|
||||
Expect.isEmpty(unreachable);
|
||||
|
||||
// Verify that the actual size of the snapshot is close to the sum of the
|
||||
// shallow sizes of all objects in the profile. They will not be exactly
|
||||
@@ -124,25 +158,14 @@ test(
|
||||
final actual = await File(strippedPath).length();
|
||||
final expected = profile.nodes.fold<int>(0, (size, n) => size + n.selfSize);
|
||||
|
||||
final bareUsed = useBare ? "bare" : "non-bare";
|
||||
final fileType = useAsm ? "assembly" : "ELF";
|
||||
String stripPrefix = "";
|
||||
if (stripFlag && stripUtil) {
|
||||
stripPrefix = "internally and externally stripped ";
|
||||
} else if (stripFlag) {
|
||||
stripPrefix = "internally stripped ";
|
||||
} else if (stripUtil) {
|
||||
stripPrefix = "externally stripped ";
|
||||
}
|
||||
|
||||
// See Elf::kPages in runtime/vm/elf.h.
|
||||
final segmentAlignment = 16384;
|
||||
final segmentAlignment = 16 * 1024;
|
||||
// Not every byte is accounted for by the snapshot profile, and data and
|
||||
// instruction segments are padded to an alignment boundary.
|
||||
final tolerance = 0.03 * actual + 2 * segmentAlignment;
|
||||
|
||||
Expect.approxEquals(expected, actual, tolerance,
|
||||
"failed on $bareUsed $stripPrefix$fileType snapshot type.");
|
||||
Expect.approxEquals(
|
||||
expected, actual, tolerance, "failed on $description snapshot");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -254,28 +277,22 @@ main() async {
|
||||
_thisTestPath
|
||||
]);
|
||||
|
||||
// Just as a reminder (these rules are applied in order inside test):
|
||||
// If useAsm is true, then stripUtil is forced (as the assembler may add
|
||||
// extra information that needs stripping).
|
||||
// If stripUtil is false, then stripFlag is forced (as the output must be
|
||||
// stripped in some way to remove DWARF information).
|
||||
|
||||
// Test stripped ELF generation directly.
|
||||
await test(
|
||||
dillPath: dillPath,
|
||||
stripFlag: true,
|
||||
stripUtil: false,
|
||||
useAsm: false,
|
||||
useBare: false);
|
||||
await test(
|
||||
dillPath: dillPath,
|
||||
stripFlag: true,
|
||||
stripUtil: false,
|
||||
useAsm: false,
|
||||
useBare: true);
|
||||
await test(dillPath);
|
||||
await test(dillPath, useBare: false);
|
||||
await test(dillPath, forceDrops: true);
|
||||
await test(dillPath, forceDrops: true, useBare: false);
|
||||
await test(dillPath, forceDrops: true, useDispatch: false);
|
||||
await test(dillPath, forceDrops: true, useDispatch: false, useBare: false);
|
||||
|
||||
// Regression test for dartbug.com/41149.
|
||||
await test(
|
||||
dillPath: dillPath,
|
||||
stripFlag: true,
|
||||
stripUtil: false,
|
||||
useAsm: false,
|
||||
useBare: false,
|
||||
disassemble: true);
|
||||
await test(dillPath, useBare: false, disassemble: true);
|
||||
|
||||
// We neither generate assembly nor have a stripping utility on Windows.
|
||||
if (Platform.isWindows) {
|
||||
@@ -288,18 +305,8 @@ main() async {
|
||||
printSkip('ELF external stripping test');
|
||||
} else {
|
||||
// Test unstripped ELF generation that is then stripped externally.
|
||||
await test(
|
||||
dillPath: dillPath,
|
||||
stripFlag: false,
|
||||
stripUtil: true,
|
||||
useAsm: false,
|
||||
useBare: false);
|
||||
await test(
|
||||
dillPath: dillPath,
|
||||
stripFlag: false,
|
||||
stripUtil: true,
|
||||
useAsm: false,
|
||||
useBare: true);
|
||||
await test(dillPath, stripUtil: true);
|
||||
await test(dillPath, stripUtil: true, useBare: false);
|
||||
}
|
||||
|
||||
// TODO(sstrickl): Currently we can't assemble for SIMARM64 on MacOSX.
|
||||
@@ -310,31 +317,11 @@ main() async {
|
||||
return;
|
||||
}
|
||||
|
||||
// Test stripped assembly generation that is then compiled and stripped.
|
||||
await test(
|
||||
dillPath: dillPath,
|
||||
stripFlag: true,
|
||||
stripUtil: true,
|
||||
useAsm: true,
|
||||
useBare: false);
|
||||
await test(
|
||||
dillPath: dillPath,
|
||||
stripFlag: true,
|
||||
stripUtil: true,
|
||||
useAsm: true,
|
||||
useBare: true);
|
||||
// Test unstripped assembly generation that is then compiled and stripped.
|
||||
await test(
|
||||
dillPath: dillPath,
|
||||
stripFlag: false,
|
||||
stripUtil: true,
|
||||
useAsm: true,
|
||||
useBare: false);
|
||||
await test(
|
||||
dillPath: dillPath,
|
||||
stripFlag: false,
|
||||
stripUtil: true,
|
||||
useAsm: true,
|
||||
useBare: true);
|
||||
await test(dillPath, useAsm: true);
|
||||
await test(dillPath, useAsm: true, useBare: false);
|
||||
// Test stripped assembly generation that is then compiled and stripped.
|
||||
await test(dillPath, useAsm: true, stripFlag: true);
|
||||
await test(dillPath, useAsm: true, stripFlag: true, useBare: false);
|
||||
});
|
||||
}
|
||||
|
||||
+112
-132
@@ -28,6 +28,7 @@
|
||||
#include "vm/stub_code.h"
|
||||
#include "vm/symbols.h"
|
||||
#include "vm/timeline.h"
|
||||
#include "vm/v8_snapshot_writer.h"
|
||||
#include "vm/version.h"
|
||||
#include "vm/zone_text_buffer.h"
|
||||
|
||||
@@ -1747,19 +1748,6 @@ class CodeSerializationCluster : public SerializationCluster {
|
||||
}
|
||||
}
|
||||
|
||||
s->Push(code->untag()->owner_);
|
||||
s->Push(code->untag()->exception_handlers_);
|
||||
s->Push(code->untag()->pc_descriptors_);
|
||||
s->Push(code->untag()->catch_entry_);
|
||||
if (s->InCurrentLoadingUnit(code->untag()->compressed_stackmaps_)) {
|
||||
s->Push(code->untag()->compressed_stackmaps_);
|
||||
}
|
||||
if (!FLAG_precompiled_mode || !FLAG_dwarf_stack_traces_mode) {
|
||||
s->Push(code->untag()->inlined_id_to_function_);
|
||||
if (s->InCurrentLoadingUnit(code->untag()->code_source_map_)) {
|
||||
s->Push(code->untag()->code_source_map_);
|
||||
}
|
||||
}
|
||||
if (s->kind() == Snapshot::kFullJIT) {
|
||||
s->Push(code->untag()->deopt_info_array_);
|
||||
s->Push(code->untag()->static_calls_target_table_);
|
||||
@@ -1793,6 +1781,29 @@ class CodeSerializationCluster : public SerializationCluster {
|
||||
UNREACHABLE();
|
||||
#endif
|
||||
}
|
||||
|
||||
if (s->InCurrentLoadingUnit(code->untag()->compressed_stackmaps_)) {
|
||||
s->Push(code->untag()->compressed_stackmaps_);
|
||||
}
|
||||
|
||||
if (Code::IsDiscarded(code)) {
|
||||
ASSERT(s->kind() == Snapshot::kFullAOT && FLAG_use_bare_instructions &&
|
||||
FLAG_dwarf_stack_traces_mode && !FLAG_retain_code_objects);
|
||||
// Only object pool and static call table entries and the compressed
|
||||
// stack maps should be pushed.
|
||||
return;
|
||||
}
|
||||
|
||||
s->Push(code->untag()->owner_);
|
||||
s->Push(code->untag()->exception_handlers_);
|
||||
s->Push(code->untag()->pc_descriptors_);
|
||||
s->Push(code->untag()->catch_entry_);
|
||||
if (!FLAG_precompiled_mode || !FLAG_dwarf_stack_traces_mode) {
|
||||
s->Push(code->untag()->inlined_id_to_function_);
|
||||
if (s->InCurrentLoadingUnit(code->untag()->code_source_map_)) {
|
||||
s->Push(code->untag()->code_source_map_);
|
||||
}
|
||||
}
|
||||
#if !defined(PRODUCT)
|
||||
s->Push(code->untag()->return_address_metadata_);
|
||||
if (FLAG_code_comments) {
|
||||
@@ -2020,7 +2031,6 @@ class CodeSerializationCluster : public SerializationCluster {
|
||||
GrowableArray<CodePtr>* objects() { return &objects_; }
|
||||
GrowableArray<CodePtr>* deferred_objects() { return &deferred_objects_; }
|
||||
|
||||
private:
|
||||
static const char* MakeDisambiguatedCodeName(Serializer* s, CodePtr c) {
|
||||
if (s->profile_writer() == nullptr) {
|
||||
return nullptr;
|
||||
@@ -2034,6 +2044,7 @@ class CodeSerializationCluster : public SerializationCluster {
|
||||
Object::NameVisibility::kInternalName));
|
||||
}
|
||||
|
||||
private:
|
||||
GrowableArray<CodePtr> objects_;
|
||||
GrowableArray<CodePtr> deferred_objects_;
|
||||
Array& array_;
|
||||
@@ -5586,17 +5597,15 @@ class VMSerializationRoots : public SerializationRoots {
|
||||
if (!should_write_symbols_ && s->profile_writer() != nullptr) {
|
||||
// If writing V8 snapshot profile create an artifical node representing
|
||||
// VM isolate symbol table.
|
||||
ASSERT(!s->IsReachable(symbols_.ptr()));
|
||||
s->AssignArtificialRef(symbols_.ptr());
|
||||
const auto& symbols_snapshot_id = s->GetProfileId(symbols_.ptr());
|
||||
s->profile_writer()->AddRoot(symbols_snapshot_id, "vm_symbols");
|
||||
s->profile_writer()->SetObjectType(symbols_snapshot_id, "Symbols");
|
||||
s->profile_writer()->SetObjectTypeAndName(symbols_snapshot_id, "Symbols",
|
||||
"vm_symbols");
|
||||
s->profile_writer()->AddRoot(symbols_snapshot_id);
|
||||
for (intptr_t i = 0; i < symbols_.Length(); i++) {
|
||||
s->profile_writer()->AttributeReferenceTo(
|
||||
symbols_snapshot_id,
|
||||
{
|
||||
V8SnapshotProfileWriter::Reference::kElement,
|
||||
{.offset = i},
|
||||
},
|
||||
symbols_snapshot_id, V8SnapshotProfileWriter::Reference::Element(i),
|
||||
s->GetProfileId(symbols_.At(i)));
|
||||
}
|
||||
}
|
||||
@@ -6096,9 +6105,6 @@ void Serializer::AddBaseObject(ObjectPtr base_object,
|
||||
num_base_objects_++;
|
||||
|
||||
if ((profile_writer_ != nullptr) && (type != nullptr)) {
|
||||
if (name == nullptr) {
|
||||
name = "<base object>";
|
||||
}
|
||||
const auto& profile_id = GetProfileId(base_object);
|
||||
profile_writer_->SetObjectTypeAndName(profile_id, type, name);
|
||||
profile_writer_->AddRoot(profile_id);
|
||||
@@ -6120,12 +6126,14 @@ intptr_t Serializer::AssignRef(ObjectPtr object) {
|
||||
}
|
||||
|
||||
intptr_t Serializer::AssignArtificialRef(ObjectPtr object) {
|
||||
ASSERT(!object.IsHeapObject() || !object.IsInstructions());
|
||||
ASSERT(heap_->GetObjectId(object) == kUnreachableReference);
|
||||
const intptr_t ref = -(next_ref_index_++);
|
||||
ASSERT(IsArtificialReference(ref));
|
||||
heap_->SetObjectId(object, ref);
|
||||
ASSERT(heap_->GetObjectId(object) == ref);
|
||||
if (object != nullptr) {
|
||||
ASSERT(!object.IsHeapObject() || !object.IsInstructions());
|
||||
ASSERT(heap_->GetObjectId(object) == kUnreachableReference);
|
||||
heap_->SetObjectId(object, ref);
|
||||
ASSERT(heap_->GetObjectId(object) == ref);
|
||||
}
|
||||
return ref;
|
||||
}
|
||||
|
||||
@@ -6141,12 +6149,16 @@ V8SnapshotProfileWriter::ObjectId Serializer::GetProfileId(
|
||||
ObjectPtr object) const {
|
||||
// Instructions are handled separately.
|
||||
ASSERT(!object->IsHeapObject() || !object->IsInstructions());
|
||||
intptr_t heap_id = UnsafeRefId(object);
|
||||
return GetProfileId(UnsafeRefId(object));
|
||||
}
|
||||
|
||||
V8SnapshotProfileWriter::ObjectId Serializer::GetProfileId(
|
||||
intptr_t heap_id) const {
|
||||
if (IsArtificialReference(heap_id)) {
|
||||
return {V8SnapshotProfileWriter::kArtificial, -heap_id};
|
||||
return {IdSpace::kArtificial, -heap_id};
|
||||
}
|
||||
ASSERT(IsAllocatedReference(heap_id));
|
||||
return {V8SnapshotProfileWriter::kSnapshot, heap_id};
|
||||
return {IdSpace::kSnapshot, heap_id};
|
||||
}
|
||||
|
||||
void Serializer::AttributeReference(
|
||||
@@ -6159,7 +6171,8 @@ void Serializer::AttributeReference(
|
||||
if (object->IsHeapObject() && object->IsWeakSerializationReference()) {
|
||||
const auto& wsr = WeakSerializationReference::RawCast(object);
|
||||
const auto& target = wsr->untag()->target();
|
||||
if (!CreateArtificialNodeIfNeeded(wsr) && HasArtificialRef(target)) {
|
||||
const bool wsr_reachable = !CreateArtificialNodeIfNeeded(wsr);
|
||||
if (wsr_reachable && HasArtificialRef(target)) {
|
||||
// The target has artificial information used for snapshot analysis and
|
||||
// the replacement is part of the snapshot, so write information for both.
|
||||
const auto& replacement = wsr->untag()->replacement();
|
||||
@@ -6168,12 +6181,11 @@ void Serializer::AttributeReference(
|
||||
GetProfileId(replacement));
|
||||
return;
|
||||
}
|
||||
// Either the target of the WSR is strongly referenced or the WSR itself is
|
||||
// unreachable, in which case it shares an artificial object ID with the
|
||||
// target due to CreateArtificialNodeIfNeeded, so fall through.
|
||||
ASSERT(HasRef(target) || HasArtificialRef(wsr));
|
||||
} else if (object_currently_writing_.id_.first ==
|
||||
V8SnapshotProfileWriter::kArtificial) {
|
||||
// The replacement isn't used, as either the target is strongly referenced
|
||||
// or the WSR itself is unreachable, so fall through to attributing a
|
||||
// reference to the WSR (which shares a profile ID with the target).
|
||||
ASSERT(GetProfileId(wsr) == GetProfileId(target));
|
||||
} else if (object_currently_writing_.id_.IsArtificial()) {
|
||||
// We may need to recur when writing members of artificial nodes in
|
||||
// CreateArtificialNodeIfNeeded.
|
||||
CreateArtificialNodeIfNeeded(object);
|
||||
@@ -6210,21 +6222,6 @@ Serializer::WritingObjectScope::~WritingObjectScope() {
|
||||
serializer_->object_currently_writing_.cid_ = old_cid_;
|
||||
}
|
||||
|
||||
V8SnapshotProfileWriter::ObjectId Serializer::WritingObjectScope::ReserveId(
|
||||
Serializer* s,
|
||||
const char* type,
|
||||
ObjectPtr obj,
|
||||
StringPtr name) {
|
||||
const char* name_str = nullptr;
|
||||
if (name != nullptr) {
|
||||
REUSABLE_STRING_HANDLESCOPE(s->thread());
|
||||
String& str = reused_string_handle.Handle();
|
||||
str = name;
|
||||
name_str = str.ToCString();
|
||||
}
|
||||
return ReserveId(s, type, obj, name_str);
|
||||
}
|
||||
|
||||
V8SnapshotProfileWriter::ObjectId Serializer::WritingObjectScope::ReserveId(
|
||||
Serializer* s,
|
||||
const char* type,
|
||||
@@ -6247,10 +6244,7 @@ V8SnapshotProfileWriter::ObjectId Serializer::WritingObjectScope::ReserveId(
|
||||
}
|
||||
case kOneByteStringCid:
|
||||
case kTwoByteStringCid: {
|
||||
REUSABLE_STRING_HANDLESCOPE(s->thread());
|
||||
String& str = reused_string_handle.Handle();
|
||||
str = String::RawCast(obj);
|
||||
name = str.ToCString();
|
||||
name = String::ToCString(s->thread(), String::RawCast(obj));
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -6294,20 +6288,12 @@ bool Serializer::CreateArtificialNodeIfNeeded(ObjectPtr obj) {
|
||||
return false;
|
||||
}
|
||||
ASSERT_EQUAL(id, kUnreachableReference);
|
||||
id = AssignArtificialRef(obj);
|
||||
|
||||
auto property = [](const char* name) -> V8SnapshotProfileWriter::Reference {
|
||||
return {V8SnapshotProfileWriter::Reference::kProperty, {.name = name}};
|
||||
};
|
||||
auto element = [](intptr_t index) -> V8SnapshotProfileWriter::Reference {
|
||||
return {V8SnapshotProfileWriter::Reference::kElement, {.offset = index}};
|
||||
};
|
||||
|
||||
const char* type = nullptr;
|
||||
StringPtr name_string = nullptr;
|
||||
const char* name = nullptr;
|
||||
GrowableArray<std::pair<ObjectPtr, V8SnapshotProfileWriter::Reference>> links;
|
||||
switch (obj->GetClassIdMayBeSmi()) {
|
||||
const classid_t cid = obj->GetClassIdMayBeSmi();
|
||||
switch (cid) {
|
||||
// For profiling static call target tables in AOT mode.
|
||||
case kSmiCid: {
|
||||
type = "Smi";
|
||||
@@ -6324,31 +6310,43 @@ bool Serializer::CreateArtificialNodeIfNeeded(ObjectPtr obj) {
|
||||
auto const elem = pool->untag()->data()[i].raw_obj_;
|
||||
// Elements should be reachable from the global object pool.
|
||||
ASSERT(HasRef(elem));
|
||||
links.Add({elem, element(i)});
|
||||
links.Add({elem, V8SnapshotProfileWriter::Reference::Element(i)});
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
// For profiling static call target tables in AOT mode.
|
||||
// For profiling static call target tables and the dispatch table in AOT.
|
||||
case kImmutableArrayCid:
|
||||
case kArrayCid: {
|
||||
type = "Array";
|
||||
auto const array = Array::RawCast(obj);
|
||||
for (intptr_t i = 0, n = Smi::Value(array->untag()->length()); i < n;
|
||||
i++) {
|
||||
ObjectPtr elem = array->untag()->data()[i];
|
||||
links.Add({elem, element(i)});
|
||||
links.Add({elem, V8SnapshotProfileWriter::Reference::Element(i)});
|
||||
}
|
||||
break;
|
||||
}
|
||||
// For profiling the dispatch table.
|
||||
case kCodeCid: {
|
||||
type = "Code";
|
||||
auto const code = Code::RawCast(obj);
|
||||
name = CodeSerializationCluster::MakeDisambiguatedCodeName(this, code);
|
||||
links.Add({code->untag()->owner(),
|
||||
V8SnapshotProfileWriter::Reference::Property("owner_")});
|
||||
break;
|
||||
}
|
||||
case kFunctionCid: {
|
||||
FunctionPtr func = static_cast<FunctionPtr>(obj);
|
||||
type = "Function";
|
||||
name = FunctionSerializationCluster::MakeDisambiguatedFunctionName(this,
|
||||
func);
|
||||
links.Add({func->untag()->owner(), property("owner_")});
|
||||
links.Add({func->untag()->owner(),
|
||||
V8SnapshotProfileWriter::Reference::Property("owner_")});
|
||||
ObjectPtr data = func->untag()->data();
|
||||
if (data->GetClassId() == kClosureDataCid) {
|
||||
links.Add({func->untag()->data(), property("data_")});
|
||||
links.Add(
|
||||
{data, V8SnapshotProfileWriter::Reference::Property("data_")});
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -6356,40 +6354,37 @@ bool Serializer::CreateArtificialNodeIfNeeded(ObjectPtr obj) {
|
||||
auto data = static_cast<ClosureDataPtr>(obj);
|
||||
type = "ClosureData";
|
||||
links.Add(
|
||||
{data->untag()->parent_function(), property("parent_function_")});
|
||||
{data->untag()->parent_function(),
|
||||
V8SnapshotProfileWriter::Reference::Property("parent_function_")});
|
||||
break;
|
||||
}
|
||||
case kClassCid: {
|
||||
ClassPtr cls = static_cast<ClassPtr>(obj);
|
||||
type = "Class";
|
||||
name_string = cls->untag()->name();
|
||||
links.Add({cls->untag()->library(), property("library_")});
|
||||
name = String::ToCString(thread(), cls->untag()->name());
|
||||
links.Add({cls->untag()->library(),
|
||||
V8SnapshotProfileWriter::Reference::Property("library_")});
|
||||
break;
|
||||
}
|
||||
case kPatchClassCid: {
|
||||
PatchClassPtr patch_cls = static_cast<PatchClassPtr>(obj);
|
||||
type = "PatchClass";
|
||||
links.Add(
|
||||
{patch_cls->untag()->patched_class(), property("patched_class_")});
|
||||
{patch_cls->untag()->patched_class(),
|
||||
V8SnapshotProfileWriter::Reference::Property("patched_class_")});
|
||||
break;
|
||||
}
|
||||
case kLibraryCid: {
|
||||
LibraryPtr lib = static_cast<LibraryPtr>(obj);
|
||||
type = "Library";
|
||||
name_string = lib->untag()->url();
|
||||
name = String::ToCString(thread(), lib->untag()->url());
|
||||
break;
|
||||
}
|
||||
default:
|
||||
UNREACHABLE();
|
||||
}
|
||||
|
||||
if (name_string != nullptr) {
|
||||
REUSABLE_STRING_HANDLESCOPE(thread());
|
||||
String& str = reused_string_handle.Handle();
|
||||
str = name_string;
|
||||
name = str.ToCString();
|
||||
FATAL("Request to create artificial node for object with cid %d", cid);
|
||||
}
|
||||
|
||||
id = AssignArtificialRef(obj);
|
||||
Serializer::WritingObjectScope scope(this, type, obj, name);
|
||||
for (const auto& link : links) {
|
||||
AttributeReference(link.first, link.second);
|
||||
@@ -6705,17 +6700,13 @@ void Serializer::WriteInstructions(InstructionsPtr instr,
|
||||
const intptr_t offset = image_writer_->GetTextOffsetFor(instr, code);
|
||||
#if defined(DART_PRECOMPILER)
|
||||
if (profile_writer_ != nullptr) {
|
||||
ASSERT(IsAllocatedReference(object_currently_writing_.id_.second));
|
||||
const auto offset_space = vm_ ? V8SnapshotProfileWriter::kVmText
|
||||
: V8SnapshotProfileWriter::kIsolateText;
|
||||
const V8SnapshotProfileWriter::ObjectId to_object(offset_space, offset);
|
||||
ASSERT(object_currently_writing_.id_ !=
|
||||
V8SnapshotProfileWriter::kArtificialRootId);
|
||||
const auto offset_space = vm_ ? IdSpace::kVmText : IdSpace::kIsolateText;
|
||||
profile_writer_->AttributeReferenceTo(
|
||||
object_currently_writing_.id_,
|
||||
{
|
||||
V8SnapshotProfileWriter::Reference::kProperty,
|
||||
{.name = "<instructions>"},
|
||||
},
|
||||
to_object);
|
||||
V8SnapshotProfileWriter::Reference::Property("<instructions>"),
|
||||
{offset_space, offset});
|
||||
}
|
||||
|
||||
if (FLAG_precompiled_mode && FLAG_use_bare_instructions) {
|
||||
@@ -6745,22 +6736,16 @@ void Serializer::WriteInstructions(InstructionsPtr instr,
|
||||
}
|
||||
|
||||
void Serializer::TraceDataOffset(uint32_t offset) {
|
||||
if (profile_writer_ != nullptr) {
|
||||
// ROData cannot be roots.
|
||||
ASSERT(IsAllocatedReference(object_currently_writing_.id_.second));
|
||||
auto offset_space = vm_ ? V8SnapshotProfileWriter::kVmData
|
||||
: V8SnapshotProfileWriter::kIsolateData;
|
||||
V8SnapshotProfileWriter::ObjectId to_object = {offset_space, offset};
|
||||
// TODO(sjindel): Give this edge a more appropriate type than element
|
||||
// (internal, maybe?).
|
||||
profile_writer_->AttributeReferenceTo(
|
||||
object_currently_writing_.id_,
|
||||
{
|
||||
V8SnapshotProfileWriter::Reference::kElement,
|
||||
{.offset = 0},
|
||||
},
|
||||
to_object);
|
||||
}
|
||||
if (profile_writer_ == nullptr) return;
|
||||
// ROData cannot be roots.
|
||||
ASSERT(object_currently_writing_.id_ !=
|
||||
V8SnapshotProfileWriter::kArtificialRootId);
|
||||
auto offset_space = vm_ ? IdSpace::kVmData : IdSpace::kIsolateData;
|
||||
// TODO(sjindel): Give this edge a more appropriate type than element
|
||||
// (internal, maybe?).
|
||||
profile_writer_->AttributeReferenceTo(
|
||||
object_currently_writing_.id_,
|
||||
V8SnapshotProfileWriter::Reference::Element(0), {offset_space, offset});
|
||||
}
|
||||
|
||||
uint32_t Serializer::GetDataOffset(ObjectPtr object) const {
|
||||
@@ -7094,13 +7079,25 @@ void Serializer::WriteDispatchTable(const Array& entries) {
|
||||
#if defined(DART_PRECOMPILER)
|
||||
if (kind() != Snapshot::kFullAOT) return;
|
||||
|
||||
AssignArtificialRef(entries.ptr());
|
||||
const auto& dispatch_table_snapshot_id = GetProfileId(entries.ptr());
|
||||
// Create an artifical node to which the bytes should be attributed. We
|
||||
// don't attribute them to entries.ptr(), as we don't want to attribute the
|
||||
// bytes for printing out a length of 0 to Object::null() when the dispatch
|
||||
// table is empty.
|
||||
const intptr_t profile_ref = AssignArtificialRef();
|
||||
const auto& dispatch_table_profile_id = GetProfileId(profile_ref);
|
||||
if (profile_writer_ != nullptr) {
|
||||
profile_writer_->AddRoot(dispatch_table_snapshot_id, "dispatch_table");
|
||||
profile_writer_->SetObjectType(dispatch_table_snapshot_id, "DispatchTable");
|
||||
profile_writer_->SetObjectTypeAndName(dispatch_table_profile_id,
|
||||
"DispatchTable", "dispatch_table");
|
||||
profile_writer_->AddRoot(dispatch_table_profile_id);
|
||||
}
|
||||
WritingObjectScope scope(this, dispatch_table_profile_id);
|
||||
if (profile_writer_ != nullptr) {
|
||||
// We'll write the Array object as a property of the artificial dispatch
|
||||
// table node, so Code objects otherwise unreferenced will have it as an
|
||||
// ancestor.
|
||||
CreateArtificialNodeIfNeeded(entries.ptr());
|
||||
AttributePropertyRef(entries.ptr(), "<code entries>");
|
||||
}
|
||||
WritingObjectScope scope(this, dispatch_table_snapshot_id);
|
||||
|
||||
const intptr_t bytes_before = bytes_written();
|
||||
const intptr_t table_length = entries.IsNull() ? 0 : entries.Length();
|
||||
@@ -7190,23 +7187,6 @@ void Serializer::WriteDispatchTable(const Array& entries) {
|
||||
Write(repeat_count);
|
||||
}
|
||||
dispatch_table_size_ = bytes_written() - bytes_before;
|
||||
|
||||
// If any bytes were written for the dispatch table, add the elements of
|
||||
// the dispatch table in the profile.
|
||||
if (profile_writer_ != nullptr && !entries.IsNull()) {
|
||||
for (intptr_t i = 0; i < entries.Length(); i++) {
|
||||
auto const code = Code::RawCast(entries.At(i));
|
||||
if (code == Code::null()) continue;
|
||||
profile_writer_->AttributeReferenceTo(
|
||||
dispatch_table_snapshot_id,
|
||||
{
|
||||
V8SnapshotProfileWriter::Reference::kElement,
|
||||
{.offset = i},
|
||||
},
|
||||
GetProfileId(code));
|
||||
}
|
||||
}
|
||||
|
||||
#endif // defined(DART_PRECOMPILER)
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
#include "vm/object.h"
|
||||
#include "vm/raw_object_fields.h"
|
||||
#include "vm/snapshot.h"
|
||||
#include "vm/v8_snapshot_writer.h"
|
||||
#include "vm/version.h"
|
||||
|
||||
#if defined(DEBUG)
|
||||
@@ -213,7 +212,7 @@ class Serializer : public ThreadStackResource {
|
||||
const char* type = nullptr,
|
||||
const char* name = nullptr);
|
||||
intptr_t AssignRef(ObjectPtr object);
|
||||
intptr_t AssignArtificialRef(ObjectPtr object);
|
||||
intptr_t AssignArtificialRef(ObjectPtr object = nullptr);
|
||||
|
||||
void Push(ObjectPtr object);
|
||||
|
||||
@@ -257,9 +256,13 @@ class Serializer : public ThreadStackResource {
|
||||
const char* type,
|
||||
ObjectPtr object,
|
||||
StringPtr name)
|
||||
: WritingObjectScope(serializer,
|
||||
ReserveId(serializer, type, object, name),
|
||||
object) {}
|
||||
: WritingObjectScope(
|
||||
serializer,
|
||||
ReserveId(serializer,
|
||||
type,
|
||||
object,
|
||||
String::ToCString(serializer->thread(), name)),
|
||||
object) {}
|
||||
|
||||
WritingObjectScope(Serializer* serializer,
|
||||
const char* type,
|
||||
@@ -281,11 +284,6 @@ class Serializer : public ThreadStackResource {
|
||||
~WritingObjectScope();
|
||||
|
||||
private:
|
||||
static V8SnapshotProfileWriter::ObjectId ReserveId(Serializer* serializer,
|
||||
const char* type,
|
||||
ObjectPtr object,
|
||||
StringPtr name);
|
||||
|
||||
static V8SnapshotProfileWriter::ObjectId ReserveId(Serializer* serializer,
|
||||
const char* type,
|
||||
ObjectPtr object,
|
||||
@@ -317,6 +315,7 @@ class Serializer : public ThreadStackResource {
|
||||
void Align(intptr_t alignment) { stream_->Align(alignment); }
|
||||
|
||||
V8SnapshotProfileWriter::ObjectId GetProfileId(ObjectPtr object) const;
|
||||
V8SnapshotProfileWriter::ObjectId GetProfileId(intptr_t ref) const;
|
||||
|
||||
void WriteRootRef(ObjectPtr object, const char* name = nullptr) {
|
||||
intptr_t id = RefId(object);
|
||||
@@ -332,8 +331,8 @@ class Serializer : public ThreadStackResource {
|
||||
const V8SnapshotProfileWriter::Reference& reference);
|
||||
|
||||
void AttributeElementRef(ObjectPtr object, intptr_t index) {
|
||||
AttributeReference(object, {V8SnapshotProfileWriter::Reference::kElement,
|
||||
{.offset = index}});
|
||||
AttributeReference(object,
|
||||
V8SnapshotProfileWriter::Reference::Element(index));
|
||||
}
|
||||
|
||||
void WriteElementRef(ObjectPtr object, intptr_t index) {
|
||||
@@ -342,8 +341,8 @@ class Serializer : public ThreadStackResource {
|
||||
}
|
||||
|
||||
void AttributePropertyRef(ObjectPtr object, const char* property) {
|
||||
AttributeReference(object, {V8SnapshotProfileWriter::Reference::kProperty,
|
||||
{.name = property}});
|
||||
AttributeReference(object,
|
||||
V8SnapshotProfileWriter::Reference::Property(property));
|
||||
}
|
||||
|
||||
void WritePropertyRef(ObjectPtr object, const char* property) {
|
||||
|
||||
+7
-7
@@ -535,8 +535,7 @@ class StringTable : public Section {
|
||||
dynamic_(allocate),
|
||||
text_(zone, 128),
|
||||
text_indices_(zone) {
|
||||
text_.AddChar('\0');
|
||||
text_indices_.Insert({"", 1});
|
||||
AddString("");
|
||||
}
|
||||
|
||||
intptr_t FileSize() const { return text_.length(); }
|
||||
@@ -549,11 +548,13 @@ class StringTable : public Section {
|
||||
|
||||
intptr_t AddString(const char* str) {
|
||||
ASSERT(str != nullptr);
|
||||
if (auto const kv = text_indices_.Lookup(str)) return kv->value - 1;
|
||||
if (auto const kv = text_indices_.Lookup(str)) {
|
||||
return kv->value;
|
||||
}
|
||||
intptr_t offset = text_.length();
|
||||
text_.AddString(str);
|
||||
text_.AddChar('\0');
|
||||
text_indices_.Insert({str, offset + 1});
|
||||
text_indices_.Insert({str, offset});
|
||||
return offset;
|
||||
}
|
||||
|
||||
@@ -562,13 +563,12 @@ class StringTable : public Section {
|
||||
return text_.buffer() + index;
|
||||
}
|
||||
intptr_t Lookup(const char* str) const {
|
||||
return text_indices_.LookupValue(str) - 1;
|
||||
return text_indices_.LookupValue(str);
|
||||
}
|
||||
|
||||
const bool dynamic_;
|
||||
ZoneTextBuffer text_;
|
||||
// To avoid kNoValue for intptr_t (0), we store an index n as n + 1.
|
||||
CStringMap<intptr_t> text_indices_;
|
||||
CStringIntMap text_indices_;
|
||||
};
|
||||
|
||||
class Symbol : public ZoneAllocated {
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ inline uint32_t CombineHashes(uint32_t hash, uint32_t other_hash) {
|
||||
return hash;
|
||||
}
|
||||
|
||||
inline uint32_t FinalizeHash(uint32_t hash, intptr_t hashbits) {
|
||||
inline uint32_t FinalizeHash(uint32_t hash, intptr_t hashbits = kBitsPerInt32) {
|
||||
hash += hash << 3;
|
||||
hash ^= hash >> 11; // Logical shift, unsigned hash.
|
||||
hash += hash << 15;
|
||||
|
||||
+31
-22
@@ -542,47 +542,56 @@ class ZoneCStringSet : public BaseCStringSet<ZoneAllocated, Zone> {
|
||||
DISALLOW_COPY_AND_ASSIGN(ZoneCStringSet);
|
||||
};
|
||||
|
||||
template <typename V>
|
||||
class CStringMapKeyValueTrait : public RawPointerKeyValueTrait<const char, V> {
|
||||
public:
|
||||
typedef typename RawPointerKeyValueTrait<const char, V>::Key Key;
|
||||
typedef typename RawPointerKeyValueTrait<const char, V>::Value Value;
|
||||
typedef typename RawPointerKeyValueTrait<const char, V>::Pair Pair;
|
||||
struct CStringIntMapKeyValueTrait {
|
||||
using Key = const char*;
|
||||
using Value = intptr_t;
|
||||
|
||||
static uword Hash(Key key) {
|
||||
static constexpr Value kNoValue = kIntptrMin;
|
||||
|
||||
struct Pair {
|
||||
Key key;
|
||||
Value value;
|
||||
Pair() : key(nullptr), value(kNoValue) {}
|
||||
Pair(const Key key, const Value& value) : key(key), value(value) {}
|
||||
Pair(const Pair& other) : key(other.key), value(other.value) {}
|
||||
Pair& operator=(const Pair&) = default;
|
||||
};
|
||||
|
||||
static Key KeyOf(const Pair& pair) { return pair.key; }
|
||||
static Value ValueOf(const Pair& pair) { return pair.value; }
|
||||
static uword Hash(const Key& key) {
|
||||
ASSERT(key != nullptr);
|
||||
return Utils::StringHash(key, strlen(key));
|
||||
}
|
||||
static bool IsKeyEqual(Pair kv, Key key) {
|
||||
static bool IsKeyEqual(const Pair& kv, const Key& key) {
|
||||
ASSERT(kv.key != nullptr && key != nullptr);
|
||||
return kv.key == key || strcmp(kv.key, key) == 0;
|
||||
}
|
||||
};
|
||||
|
||||
template <typename V, typename B, typename Allocator>
|
||||
class BaseCStringMap
|
||||
: public BaseDirectChainedHashMap<CStringMapKeyValueTrait<V>,
|
||||
template <typename B, typename Allocator>
|
||||
class BaseCStringIntMap
|
||||
: public BaseDirectChainedHashMap<CStringIntMapKeyValueTrait,
|
||||
B,
|
||||
Allocator> {
|
||||
public:
|
||||
explicit BaseCStringMap(Allocator* allocator)
|
||||
: BaseDirectChainedHashMap<CStringMapKeyValueTrait<V>, B, Allocator>(
|
||||
explicit BaseCStringIntMap(Allocator* allocator)
|
||||
: BaseDirectChainedHashMap<CStringIntMapKeyValueTrait, B, Allocator>(
|
||||
allocator) {}
|
||||
|
||||
private:
|
||||
DISALLOW_COPY_AND_ASSIGN(BaseCStringMap);
|
||||
DISALLOW_COPY_AND_ASSIGN(BaseCStringIntMap);
|
||||
};
|
||||
|
||||
template <typename V>
|
||||
class CStringMap : public BaseCStringMap<V, ValueObject, Zone> {
|
||||
class CStringIntMap : public BaseCStringIntMap<ValueObject, Zone> {
|
||||
public:
|
||||
CStringMap()
|
||||
: BaseCStringMap<V, ValueObject, Zone>(ThreadState::Current()->zone()) {}
|
||||
explicit CStringMap(Zone* zone)
|
||||
: BaseCStringMap<V, ValueObject, Zone>(zone) {}
|
||||
CStringIntMap()
|
||||
: BaseCStringIntMap<ValueObject, Zone>(ThreadState::Current()->zone()) {}
|
||||
explicit CStringIntMap(Zone* zone)
|
||||
: BaseCStringIntMap<ValueObject, Zone>(zone) {}
|
||||
|
||||
private:
|
||||
DISALLOW_COPY_AND_ASSIGN(CStringMap);
|
||||
DISALLOW_COPY_AND_ASSIGN(CStringIntMap);
|
||||
};
|
||||
|
||||
template <typename V>
|
||||
@@ -653,7 +662,7 @@ class IdentitySetKeyValueTrait {
|
||||
static Value ValueOf(Pair kv) { return kv; }
|
||||
|
||||
static inline uword Hash(Key key) {
|
||||
return Utils::WordHash(reinterpret_cast<intptr_t>(key));
|
||||
return Utils::StringHash(reinterpret_cast<const char*>(&key), sizeof(key));
|
||||
}
|
||||
|
||||
static inline bool IsKeyEqual(Pair pair, Key key) { return pair == key; }
|
||||
|
||||
@@ -259,7 +259,7 @@ TEST_CASE(ZoneCStringSet) {
|
||||
EXPECT(set->IsEmpty());
|
||||
}
|
||||
|
||||
TEST_CASE(CStringMap) {
|
||||
TEST_CASE(CStringIntMap) {
|
||||
const char* const kConst1 = "test";
|
||||
const char* const kConst2 = "test 2";
|
||||
|
||||
@@ -274,7 +274,7 @@ TEST_CASE(CStringMap) {
|
||||
const intptr_t i1 = 1;
|
||||
const intptr_t i2 = 2;
|
||||
|
||||
CStringMap<intptr_t> map;
|
||||
CStringIntMap map;
|
||||
EXPECT(map.IsEmpty());
|
||||
|
||||
map.Insert({str1, i1});
|
||||
@@ -307,7 +307,7 @@ TEST_CASE(CStringMap) {
|
||||
free(str1);
|
||||
}
|
||||
|
||||
TEST_CASE(CStringMapUpdate) {
|
||||
TEST_CASE(CStringIntMapUpdate) {
|
||||
const char* const kConst1 = "test";
|
||||
const char* const kConst2 = "test 2";
|
||||
|
||||
@@ -323,11 +323,11 @@ TEST_CASE(CStringMapUpdate) {
|
||||
EXPECT_STREQ(str1, str3);
|
||||
EXPECT_STREQ(str1, str4);
|
||||
|
||||
CStringMapKeyValueTrait<intptr_t>::Pair p1 = {str1, 1};
|
||||
CStringMapKeyValueTrait<intptr_t>::Pair p2 = {str2, 2};
|
||||
CStringMapKeyValueTrait<intptr_t>::Pair p3 = {str3, 3};
|
||||
CStringIntMapKeyValueTrait::Pair p1{str1, 1};
|
||||
CStringIntMapKeyValueTrait::Pair p2{str2, 2};
|
||||
CStringIntMapKeyValueTrait::Pair p3{str3, 3};
|
||||
|
||||
CStringMap<intptr_t> map;
|
||||
CStringIntMap map;
|
||||
EXPECT(map.IsEmpty());
|
||||
|
||||
map.Update(p1);
|
||||
|
||||
@@ -462,15 +462,13 @@ void ImageWriter::Write(NonStreamingWriteStream* clustered_stream, bool vm) {
|
||||
// BSSsection in the text section as an initial InstructionsSection object.
|
||||
WriteBss(vm);
|
||||
|
||||
offset_space_ = vm ? V8SnapshotProfileWriter::kVmText
|
||||
: V8SnapshotProfileWriter::kIsolateText;
|
||||
offset_space_ = vm ? IdSpace::kVmText : IdSpace::kIsolateText;
|
||||
WriteText(vm);
|
||||
|
||||
// Append the direct-mapped RO data objects after the clustered snapshot
|
||||
// and then for ELF and assembly outputs, add appropriate sections with
|
||||
// that combined data.
|
||||
offset_space_ = vm ? V8SnapshotProfileWriter::kVmData
|
||||
: V8SnapshotProfileWriter::kIsolateData;
|
||||
offset_space_ = vm ? IdSpace::kVmData : IdSpace::kIsolateData;
|
||||
WriteROData(clustered_stream, vm);
|
||||
}
|
||||
|
||||
@@ -680,14 +678,10 @@ void ImageWriter::WriteText(bool vm) {
|
||||
instructions_symbol);
|
||||
profile_writer_->AttributeBytesTo(id,
|
||||
section_size - section_payload_length);
|
||||
const intptr_t element_offset = id.second - parent_id.second;
|
||||
const intptr_t element_offset = id.nonce() - parent_id.nonce();
|
||||
profile_writer_->AttributeReferenceTo(
|
||||
parent_id,
|
||||
{
|
||||
V8SnapshotProfileWriter::Reference::kElement,
|
||||
{.offset = element_offset},
|
||||
},
|
||||
id);
|
||||
V8SnapshotProfileWriter::Reference::Element(element_offset), id);
|
||||
// Later objects will have the InstructionsSection as a parent if in
|
||||
// bare instructions mode, otherwise the image.
|
||||
if (bare_instruction_payloads) {
|
||||
@@ -718,7 +712,7 @@ void ImageWriter::WriteText(bool vm) {
|
||||
? compiler::target::InstructionsSection::HeaderSize()
|
||||
: compiler::target::InstructionsSection::InstanceSize(0);
|
||||
text_offset += Align(section_contents_alignment, text_offset);
|
||||
ASSERT_EQUAL(text_offset - id.second, expected_size);
|
||||
ASSERT_EQUAL(text_offset - id.nonce(), expected_size);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -729,7 +723,7 @@ void ImageWriter::WriteText(bool vm) {
|
||||
SnapshotTextObjectNamer namer(zone);
|
||||
#endif
|
||||
|
||||
ASSERT(offset_space_ != V8SnapshotProfileWriter::kSnapshot);
|
||||
ASSERT(offset_space_ != IdSpace::kSnapshot);
|
||||
for (intptr_t i = 0; i < instructions_.length(); i++) {
|
||||
auto& data = instructions_[i];
|
||||
const bool is_trampoline = data.trampoline_bytes != nullptr;
|
||||
@@ -748,14 +742,10 @@ void ImageWriter::WriteText(bool vm) {
|
||||
: SizeInSnapshot(data.insns_->ptr());
|
||||
profile_writer_->SetObjectTypeAndName(id, type, object_name);
|
||||
profile_writer_->AttributeBytesTo(id, size);
|
||||
const intptr_t element_offset = id.second - parent_id.second;
|
||||
const intptr_t element_offset = id.nonce() - parent_id.nonce();
|
||||
profile_writer_->AttributeReferenceTo(
|
||||
parent_id,
|
||||
{
|
||||
V8SnapshotProfileWriter::Reference::kElement,
|
||||
{.offset = element_offset},
|
||||
},
|
||||
id);
|
||||
V8SnapshotProfileWriter::Reference::Element(element_offset), id);
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@@ -269,10 +269,10 @@ class ImageWriter : public ValueObject {
|
||||
void PrepareForSerialization(GrowableArray<ImageWriterCommand>* commands);
|
||||
|
||||
bool IsROSpace() const {
|
||||
return offset_space_ == V8SnapshotProfileWriter::kVmData ||
|
||||
offset_space_ == V8SnapshotProfileWriter::kVmText ||
|
||||
offset_space_ == V8SnapshotProfileWriter::kIsolateData ||
|
||||
offset_space_ == V8SnapshotProfileWriter::kIsolateText;
|
||||
return offset_space_ == IdSpace::kVmData ||
|
||||
offset_space_ == IdSpace::kVmText ||
|
||||
offset_space_ == IdSpace::kIsolateData ||
|
||||
offset_space_ == IdSpace::kIsolateText;
|
||||
}
|
||||
int32_t GetTextOffsetFor(InstructionsPtr instructions, CodePtr code);
|
||||
uint32_t GetDataOffsetFor(ObjectPtr raw_object);
|
||||
@@ -436,8 +436,7 @@ class ImageWriter : public ValueObject {
|
||||
GrowableArray<ObjectData> objects_;
|
||||
GrowableArray<InstructionsData> instructions_;
|
||||
|
||||
V8SnapshotProfileWriter::IdSpace offset_space_ =
|
||||
V8SnapshotProfileWriter::kSnapshot;
|
||||
IdSpace offset_space_ = IdSpace::kSnapshot;
|
||||
V8SnapshotProfileWriter* profile_writer_ = nullptr;
|
||||
const char* const image_type_;
|
||||
const char* const instructions_section_type_;
|
||||
|
||||
@@ -23096,6 +23096,14 @@ void String::ToUTF8(uint8_t* utf8_array, intptr_t array_len) const {
|
||||
Utf8::Encode(*this, reinterpret_cast<char*>(utf8_array), array_len);
|
||||
}
|
||||
|
||||
const char* String::ToCString(Thread* thread, StringPtr ptr) {
|
||||
if (ptr == nullptr) return nullptr;
|
||||
REUSABLE_STRING_HANDLESCOPE(thread);
|
||||
String& str = reused_string_handle.Handle();
|
||||
str = ptr;
|
||||
return str.ToCString();
|
||||
}
|
||||
|
||||
static FinalizablePersistentHandle* AddFinalizer(const Object& referent,
|
||||
void* peer,
|
||||
Dart_HandleFinalizer callback,
|
||||
|
||||
@@ -9079,6 +9079,7 @@ class String : public Instance {
|
||||
|
||||
char* ToMallocCString() const;
|
||||
void ToUTF8(uint8_t* utf8_array, intptr_t array_len) const;
|
||||
static const char* ToCString(Thread* thread, StringPtr ptr);
|
||||
|
||||
// Creates a new String object from a C string that is assumed to contain
|
||||
// UTF-8 encoded characters and '\0' is considered a termination character.
|
||||
|
||||
+239
-135
@@ -10,109 +10,85 @@
|
||||
namespace dart {
|
||||
|
||||
const V8SnapshotProfileWriter::ObjectId
|
||||
V8SnapshotProfileWriter::kArtificialRootId{kArtificial, 0};
|
||||
V8SnapshotProfileWriter::kArtificialRootId{IdSpace::kArtificial, 0};
|
||||
|
||||
#if defined(DART_PRECOMPILER)
|
||||
|
||||
static const char* ZoneString(Zone* Z, const char* str) {
|
||||
return OS::SCreate(Z, "%s", str);
|
||||
}
|
||||
|
||||
V8SnapshotProfileWriter::V8SnapshotProfileWriter(Zone* zone)
|
||||
: zone_(zone),
|
||||
node_types_(zone_),
|
||||
edge_types_(zone_),
|
||||
strings_(zone_),
|
||||
roots_(zone_) {
|
||||
node_types_.Insert({"Unknown", kUnknown});
|
||||
node_types_.Insert({"ArtificialRoot", kArtificialRoot});
|
||||
intptr_t idx = edge_types_.Add("context");
|
||||
ASSERT_EQUAL(idx, static_cast<intptr_t>(Edge::Type::kContext));
|
||||
idx = edge_types_.Add("element");
|
||||
ASSERT_EQUAL(idx, static_cast<intptr_t>(Edge::Type::kElement));
|
||||
idx = edge_types_.Add("property");
|
||||
ASSERT_EQUAL(idx, static_cast<intptr_t>(Edge::Type::kProperty));
|
||||
idx = edge_types_.Add("internal");
|
||||
ASSERT_EQUAL(idx, static_cast<intptr_t>(Edge::Type::kInternal));
|
||||
|
||||
edge_types_.Insert({"context", kContext});
|
||||
edge_types_.Insert({"element", kElement});
|
||||
edge_types_.Insert({"property", kProperty});
|
||||
edge_types_.Insert({"internal", kInternal});
|
||||
|
||||
strings_.Insert({"<unknown>", kUnknownString});
|
||||
strings_.Insert({"<artificial root>", kArtificialRootString});
|
||||
|
||||
nodes_.Insert(NodeInfo(zone_, kArtificialRoot, kArtificialRootString,
|
||||
kArtificialRootId, 0, 0));
|
||||
SetObjectTypeAndName(kArtificialRootId, "ArtificialRoot",
|
||||
"<artificial root>");
|
||||
}
|
||||
|
||||
void V8SnapshotProfileWriter::SetObjectTypeAndName(ObjectId object_id,
|
||||
void V8SnapshotProfileWriter::SetObjectTypeAndName(const ObjectId& object_id,
|
||||
const char* type,
|
||||
const char* name) {
|
||||
ASSERT(type != nullptr);
|
||||
|
||||
if (!node_types_.HasKey(type)) {
|
||||
node_types_.Insert({ZoneString(zone_, type), node_types_.Size()});
|
||||
}
|
||||
|
||||
intptr_t type_id = node_types_.LookupValue(type);
|
||||
NodeInfo* info = EnsureId(object_id);
|
||||
ASSERT(info->type == kUnknown || info->type == type_id);
|
||||
info->type = type_id;
|
||||
if (name != nullptr) {
|
||||
info->name = EnsureString(name);
|
||||
} else {
|
||||
info->name =
|
||||
EnsureString(OS::SCreate(zone_, "Unnamed [%s] %s", type, "(nil)"));
|
||||
const intptr_t type_index = node_types_.Add(type);
|
||||
if (info->type != kInvalidString && info->type != type_index) {
|
||||
FATAL("Attempting to assign mismatching type %s to node %s", type,
|
||||
info->ToCString(zone_));
|
||||
}
|
||||
info->type = type_index;
|
||||
// Don't overwrite any existing name.
|
||||
if (info->name == kInvalidString) {
|
||||
info->name = strings_.Add(name);
|
||||
}
|
||||
}
|
||||
|
||||
void V8SnapshotProfileWriter::AttributeBytesTo(ObjectId object_id,
|
||||
void V8SnapshotProfileWriter::AttributeBytesTo(const ObjectId& object_id,
|
||||
size_t num_bytes) {
|
||||
EnsureId(object_id)->self_size += num_bytes;
|
||||
}
|
||||
|
||||
V8SnapshotProfileWriter::ConstantEdgeType
|
||||
V8SnapshotProfileWriter::ReferenceTypeToEdgeType(Reference::Type type) {
|
||||
switch (type) {
|
||||
case Reference::kElement:
|
||||
return ConstantEdgeType::kElement;
|
||||
case Reference::kProperty:
|
||||
return ConstantEdgeType::kProperty;
|
||||
}
|
||||
}
|
||||
|
||||
void V8SnapshotProfileWriter::AttributeReferenceTo(ObjectId from_object_id,
|
||||
Reference reference,
|
||||
ObjectId to_object_id) {
|
||||
const bool is_element = reference.reference_type == Reference::kElement;
|
||||
ASSERT(is_element ? reference.offset >= 0 : reference.name != nullptr);
|
||||
|
||||
void V8SnapshotProfileWriter::AttributeReferenceTo(
|
||||
const ObjectId& from_object_id,
|
||||
const Reference& reference,
|
||||
const ObjectId& to_object_id) {
|
||||
ASSERT(reference.IsElement() ? reference.offset >= 0
|
||||
: reference.name != nullptr);
|
||||
EnsureId(to_object_id);
|
||||
const Edge edge(ReferenceTypeToEdgeType(reference.reference_type),
|
||||
is_element ? reference.offset : EnsureString(reference.name));
|
||||
const Edge edge(this, reference);
|
||||
EnsureId(from_object_id)->AddEdge(edge, to_object_id);
|
||||
++edge_count_;
|
||||
}
|
||||
|
||||
void V8SnapshotProfileWriter::AttributeDroppedReferenceTo(
|
||||
ObjectId from_object_id,
|
||||
Reference reference,
|
||||
ObjectId to_object_id,
|
||||
ObjectId replacement_object_id) {
|
||||
ASSERT(to_object_id.first == kArtificial);
|
||||
ASSERT(replacement_object_id.first != kArtificial);
|
||||
|
||||
const bool is_element = reference.reference_type == Reference::kElement;
|
||||
ASSERT(is_element ? reference.offset >= 0 : reference.name != nullptr);
|
||||
const ObjectId& from_object_id,
|
||||
const Reference& reference,
|
||||
const ObjectId& to_object_id,
|
||||
const ObjectId& replacement_object_id) {
|
||||
ASSERT(to_object_id.IsArtificial());
|
||||
ASSERT(!replacement_object_id.IsArtificial());
|
||||
ASSERT(reference.IsElement() ? reference.offset >= 0
|
||||
: reference.name != nullptr);
|
||||
|
||||
// The target node is added normally.
|
||||
AttributeReferenceTo(from_object_id, reference, to_object_id);
|
||||
|
||||
EnsureId(replacement_object_id);
|
||||
// Put the replacement node at an invalid offset or name that can still be
|
||||
// associated with the real one. For offsets, this is the negative offset.
|
||||
// For names, it's the name prefixed with ":replacement_".
|
||||
EnsureId(replacement_object_id);
|
||||
const Edge replacement_edge(
|
||||
ReferenceTypeToEdgeType(reference.reference_type),
|
||||
is_element ? -reference.offset
|
||||
: EnsureString(
|
||||
OS::SCreate(zone_, ":replacement_%s", reference.name)));
|
||||
Reference replacement_reference =
|
||||
reference.IsElement() ? Reference::Element(-reference.offset)
|
||||
: Reference::Property(OS::SCreate(
|
||||
zone_, ":replacement_%s", reference.name));
|
||||
const Edge replacement_edge(this, replacement_reference);
|
||||
EnsureId(from_object_id)->AddEdge(replacement_edge, replacement_object_id);
|
||||
++edge_count_;
|
||||
}
|
||||
|
||||
bool V8SnapshotProfileWriter::HasId(const ObjectId& object_id) {
|
||||
@@ -120,72 +96,195 @@ bool V8SnapshotProfileWriter::HasId(const ObjectId& object_id) {
|
||||
}
|
||||
|
||||
V8SnapshotProfileWriter::NodeInfo* V8SnapshotProfileWriter::EnsureId(
|
||||
ObjectId object_id) {
|
||||
const ObjectId& object_id) {
|
||||
if (!HasId(object_id)) {
|
||||
nodes_.Insert(NodeInfo(zone_, kUnknown, kUnknownString, object_id, 0, -1));
|
||||
nodes_.Insert(NodeInfo(this, object_id));
|
||||
}
|
||||
return nodes_.Lookup(object_id);
|
||||
}
|
||||
|
||||
intptr_t V8SnapshotProfileWriter::EnsureString(const char* str) {
|
||||
if (!strings_.HasKey(str)) {
|
||||
strings_.Insert({ZoneString(zone_, str), strings_.Size()});
|
||||
return strings_.Size() - 1;
|
||||
const char* V8SnapshotProfileWriter::NodeInfo::ToCString(Zone* zone) const {
|
||||
JSONWriter writer;
|
||||
WriteDebug(&writer);
|
||||
return OS::SCreate(zone, "%s", writer.buffer()->buffer());
|
||||
}
|
||||
|
||||
void V8SnapshotProfileWriter::NodeInfo::Write(JSONWriter* writer) const {
|
||||
ASSERT(id.space() != IdSpace::kInvalid);
|
||||
if (type == kInvalidString) {
|
||||
FATAL("No type given for node %s", id.ToCString(profile_writer_->zone_));
|
||||
}
|
||||
return strings_.LookupValue(str);
|
||||
writer->PrintValue(type);
|
||||
if (name != kInvalidString) {
|
||||
writer->PrintValue(name);
|
||||
} else {
|
||||
ASSERT(profile_writer_ != nullptr);
|
||||
// If we don't already have a name for the node, we lazily create a default
|
||||
// one. This is safe since the strings table is written out after the nodes.
|
||||
const intptr_t name = profile_writer_->strings_.AddFormatted(
|
||||
"Unnamed [%s] (nil)", profile_writer_->node_types_.At(type));
|
||||
writer->PrintValue(name);
|
||||
}
|
||||
id.Write(writer);
|
||||
writer->PrintValue(self_size);
|
||||
writer->PrintValue64(edges->Length());
|
||||
}
|
||||
|
||||
intptr_t V8SnapshotProfileWriter::WriteNodeInfo(JSONWriter* writer,
|
||||
const NodeInfo& info) {
|
||||
writer->PrintValue(info.type);
|
||||
writer->PrintValue(info.name);
|
||||
writer->PrintValue(NodeIdFor(info.id));
|
||||
writer->PrintValue(info.self_size);
|
||||
writer->PrintValue64(info.edges->Length());
|
||||
writer->PrintNewline();
|
||||
return kNumNodeFields;
|
||||
void V8SnapshotProfileWriter::NodeInfo::WriteDebug(JSONWriter* writer) const {
|
||||
writer->OpenObject();
|
||||
if (type != kInvalidString) {
|
||||
writer->PrintProperty("type", profile_writer_->node_types_.At(type));
|
||||
}
|
||||
if (name != kInvalidString) {
|
||||
writer->PrintProperty("name", profile_writer_->strings_.At(name));
|
||||
}
|
||||
id.WriteDebug(writer, "id");
|
||||
writer->PrintProperty("self_size", self_size);
|
||||
edges->WriteDebug(writer, "edges");
|
||||
writer->CloseObject();
|
||||
}
|
||||
|
||||
void V8SnapshotProfileWriter::WriteEdgeInfo(JSONWriter* writer,
|
||||
const Edge& info,
|
||||
const ObjectId& target) {
|
||||
writer->PrintValue64(info.first);
|
||||
writer->PrintValue64(info.second);
|
||||
writer->PrintValue64(nodes_.LookupValue(target).offset);
|
||||
writer->PrintNewline();
|
||||
const char* V8SnapshotProfileWriter::ObjectId::ToCString(Zone* zone) const {
|
||||
JSONWriter writer;
|
||||
WriteDebug(&writer);
|
||||
return OS::SCreate(zone, "%s", writer.buffer()->buffer());
|
||||
}
|
||||
|
||||
void V8SnapshotProfileWriter::AddRoot(ObjectId object_id,
|
||||
const char* name /*= nullptr*/) {
|
||||
EnsureId(object_id);
|
||||
void V8SnapshotProfileWriter::ObjectId::Write(JSONWriter* writer,
|
||||
const char* property) const {
|
||||
if (property != nullptr) {
|
||||
writer->PrintProperty64(property, encoded_);
|
||||
} else {
|
||||
writer->PrintValue64(encoded_);
|
||||
}
|
||||
}
|
||||
|
||||
void V8SnapshotProfileWriter::ObjectId::WriteDebug(JSONWriter* writer,
|
||||
const char* property) const {
|
||||
writer->OpenObject(property);
|
||||
writer->PrintProperty("space", IdSpaceToCString(space()));
|
||||
writer->PrintProperty64("nonce", nonce());
|
||||
writer->CloseObject();
|
||||
}
|
||||
|
||||
const char* V8SnapshotProfileWriter::ObjectId::IdSpaceToCString(IdSpace space) {
|
||||
switch (space) {
|
||||
case IdSpace::kInvalid:
|
||||
return "Invalid";
|
||||
case IdSpace::kSnapshot:
|
||||
return "Snapshot";
|
||||
case IdSpace::kVmText:
|
||||
return "VmText";
|
||||
case IdSpace::kIsolateText:
|
||||
return "IsolateText";
|
||||
case IdSpace::kVmData:
|
||||
return "VmData";
|
||||
case IdSpace::kIsolateData:
|
||||
return "IsolateData";
|
||||
case IdSpace::kArtificial:
|
||||
return "Artificial";
|
||||
}
|
||||
}
|
||||
|
||||
const char* V8SnapshotProfileWriter::EdgeMap::ToCString(Zone* zone) const {
|
||||
JSONWriter writer;
|
||||
WriteDebug(&writer);
|
||||
return OS::SCreate(zone, "%s", writer.buffer()->buffer());
|
||||
}
|
||||
|
||||
void V8SnapshotProfileWriter::EdgeMap::WriteDebug(JSONWriter* writer,
|
||||
const char* property) const {
|
||||
writer->OpenArray(property);
|
||||
auto edge_it = GetIterator();
|
||||
while (auto const pair = edge_it.Next()) {
|
||||
pair->edge.WriteDebug(writer, pair->target);
|
||||
}
|
||||
writer->CloseArray();
|
||||
}
|
||||
|
||||
void V8SnapshotProfileWriter::Edge::Write(JSONWriter* writer,
|
||||
const ObjectId& target_id) const {
|
||||
ASSERT(type != Type::kInvalid);
|
||||
writer->PrintValue64(static_cast<intptr_t>(type));
|
||||
writer->PrintValue64(name_or_offset);
|
||||
auto const target = profile_writer_->nodes_.LookupValue(target_id);
|
||||
writer->PrintValue64(target.offset());
|
||||
}
|
||||
|
||||
void V8SnapshotProfileWriter::Edge::WriteDebug(
|
||||
JSONWriter* writer,
|
||||
const ObjectId& target_id) const {
|
||||
writer->OpenObject();
|
||||
if (type != Type::kInvalid) {
|
||||
writer->PrintProperty(
|
||||
"type", profile_writer_->edge_types_.At(static_cast<intptr_t>(type)));
|
||||
}
|
||||
if (type == Type::kProperty) {
|
||||
writer->PrintProperty("name", profile_writer_->strings_.At(name_or_offset));
|
||||
} else {
|
||||
writer->PrintProperty64("offset", name_or_offset);
|
||||
}
|
||||
auto const target = profile_writer_->nodes_.LookupValue(target_id);
|
||||
target.id.WriteDebug(writer, "target");
|
||||
writer->CloseObject();
|
||||
}
|
||||
|
||||
void V8SnapshotProfileWriter::AddRoot(const ObjectId& object_id,
|
||||
const char* name) {
|
||||
// HeapSnapshotWorker.HeapSnapshot.calculateDistances (from HeapSnapshot.js)
|
||||
// assumes that the root does not have more than one edge to any other node
|
||||
// (most likely an oversight).
|
||||
if (roots_.HasKey(object_id)) return;
|
||||
roots_.Insert(object_id);
|
||||
|
||||
auto const info = NodeInfo(
|
||||
zone_, 0, name != nullptr ? EnsureString(name) : -1, object_id, 0, 0);
|
||||
roots_.Insert(info);
|
||||
auto const root = EnsureId(kArtificialRootId);
|
||||
root->AddEdge(info.name != -1 ? Edge(kProperty, info.name)
|
||||
: Edge(kInternal, root->edges->Length()),
|
||||
auto const str_index = strings_.Add(name);
|
||||
auto const root = nodes_.Lookup(kArtificialRootId);
|
||||
ASSERT(root != nullptr);
|
||||
root->AddEdge(str_index != kInvalidString
|
||||
? Edge(this, Edge::Type::kProperty, str_index)
|
||||
: Edge(this, Edge::Type::kInternal, root->edges->Length()),
|
||||
object_id);
|
||||
}
|
||||
|
||||
void V8SnapshotProfileWriter::WriteStringsTable(
|
||||
JSONWriter* writer,
|
||||
const DirectChainedHashMap<StringToIntMapTraits>& map) {
|
||||
const char** strings = zone_->Alloc<const char*>(map.Size());
|
||||
StringToIntMapTraits::Pair* pair = nullptr;
|
||||
auto it = map.GetIterator();
|
||||
while ((pair = it.Next()) != nullptr) {
|
||||
ASSERT(pair->value >= 0 && pair->value < map.Size());
|
||||
strings[pair->value] = pair->key;
|
||||
intptr_t V8SnapshotProfileWriter::StringsTable::Add(const char* str) {
|
||||
if (str == nullptr) return kInvalidString;
|
||||
if (auto const kv = index_map_.Lookup(str)) {
|
||||
return kv->value;
|
||||
}
|
||||
for (intptr_t i = 0; i < map.Size(); ++i) {
|
||||
writer->PrintValue(strings[i]);
|
||||
const char* new_str = OS::SCreate(zone_, "%s", str);
|
||||
const intptr_t index = strings_.length();
|
||||
strings_.Add(new_str);
|
||||
index_map_.Insert({new_str, index});
|
||||
return index;
|
||||
}
|
||||
|
||||
intptr_t V8SnapshotProfileWriter::StringsTable::AddFormatted(const char* fmt,
|
||||
...) {
|
||||
va_list args;
|
||||
va_start(args, fmt);
|
||||
const char* str = OS::VSCreate(zone_, fmt, args);
|
||||
if (auto const kv = index_map_.Lookup(str)) {
|
||||
return kv->value;
|
||||
}
|
||||
const intptr_t index = strings_.length();
|
||||
strings_.Add(str);
|
||||
index_map_.Insert({str, index});
|
||||
return index;
|
||||
}
|
||||
|
||||
const char* V8SnapshotProfileWriter::StringsTable::At(intptr_t index) const {
|
||||
if (index > strings_.length()) return nullptr;
|
||||
return strings_[index];
|
||||
}
|
||||
|
||||
void V8SnapshotProfileWriter::StringsTable::Write(JSONWriter* writer,
|
||||
const char* property) const {
|
||||
writer->OpenArray(property);
|
||||
for (auto const str : strings_) {
|
||||
writer->PrintValue(str);
|
||||
writer->PrintNewline();
|
||||
}
|
||||
writer->CloseArray();
|
||||
}
|
||||
|
||||
void V8SnapshotProfileWriter::Write(JSONWriter* writer) {
|
||||
@@ -207,11 +306,7 @@ void V8SnapshotProfileWriter::Write(JSONWriter* writer) {
|
||||
|
||||
{
|
||||
writer->OpenArray("node_types");
|
||||
{
|
||||
writer->OpenArray();
|
||||
WriteStringsTable(writer, node_types_);
|
||||
writer->CloseArray();
|
||||
}
|
||||
node_types_.Write(writer);
|
||||
writer->CloseArray();
|
||||
}
|
||||
|
||||
@@ -225,48 +320,59 @@ void V8SnapshotProfileWriter::Write(JSONWriter* writer) {
|
||||
|
||||
{
|
||||
writer->OpenArray("edge_types");
|
||||
{
|
||||
writer->OpenArray();
|
||||
WriteStringsTable(writer, edge_types_);
|
||||
writer->CloseArray();
|
||||
}
|
||||
edge_types_.Write(writer);
|
||||
writer->CloseArray();
|
||||
}
|
||||
|
||||
writer->CloseObject();
|
||||
|
||||
writer->PrintProperty64("node_count", nodes_.Size());
|
||||
writer->PrintProperty64("edge_count", edge_count_ + roots_.Size());
|
||||
{
|
||||
intptr_t edge_count = 0;
|
||||
auto nodes_it = nodes_.GetIterator();
|
||||
while (auto const info = nodes_it.Next()) {
|
||||
// All nodes should have an edge map, though it may be empty.
|
||||
ASSERT(info->edges != nullptr);
|
||||
edge_count += info->edges->Length();
|
||||
}
|
||||
writer->PrintProperty64("edge_count", edge_count);
|
||||
}
|
||||
}
|
||||
writer->CloseObject();
|
||||
|
||||
const auto& root = *nodes_.Lookup(kArtificialRootId);
|
||||
auto nodes_it = nodes_.GetIterator();
|
||||
|
||||
{
|
||||
writer->OpenArray("nodes");
|
||||
// Always write the information for the artificial root first.
|
||||
intptr_t offset = WriteNodeInfo(writer, root);
|
||||
auto const root = nodes_.Lookup(kArtificialRootId);
|
||||
ASSERT(root != nullptr);
|
||||
intptr_t offset = 0;
|
||||
root->set_offset(offset);
|
||||
root->Write(writer);
|
||||
offset += kNumNodeFields;
|
||||
auto nodes_it = nodes_.GetIterator();
|
||||
for (auto entry = nodes_it.Next(); entry != nullptr;
|
||||
entry = nodes_it.Next()) {
|
||||
if (entry->id == kArtificialRootId) continue;
|
||||
entry->offset = offset;
|
||||
offset += WriteNodeInfo(writer, *entry);
|
||||
entry->set_offset(offset);
|
||||
entry->Write(writer);
|
||||
offset += kNumNodeFields;
|
||||
}
|
||||
writer->CloseArray();
|
||||
nodes_it.Reset();
|
||||
}
|
||||
|
||||
{
|
||||
auto write_edges = [&](const NodeInfo& info) {
|
||||
auto edges_it = info.edges->GetIterator();
|
||||
while (auto const pair = edges_it.Next()) {
|
||||
WriteEdgeInfo(writer, pair->edge, pair->target);
|
||||
pair->edge.Write(writer, pair->target);
|
||||
}
|
||||
};
|
||||
writer->OpenArray("edges");
|
||||
// Always write the information for the artificial root first.
|
||||
write_edges(root);
|
||||
auto const root = nodes_.Lookup(kArtificialRootId);
|
||||
ASSERT(root != nullptr);
|
||||
write_edges(*root);
|
||||
auto nodes_it = nodes_.GetIterator();
|
||||
while (auto const entry = nodes_it.Next()) {
|
||||
if (entry->id == kArtificialRootId) continue;
|
||||
write_edges(*entry);
|
||||
@@ -274,11 +380,9 @@ void V8SnapshotProfileWriter::Write(JSONWriter* writer) {
|
||||
writer->CloseArray();
|
||||
}
|
||||
|
||||
{
|
||||
writer->OpenArray("strings");
|
||||
WriteStringsTable(writer, strings_);
|
||||
writer->CloseArray();
|
||||
}
|
||||
// Must happen after any calls to WriteNodeInfo, as those calls may add more
|
||||
// strings.
|
||||
strings_.Write(writer, "strings");
|
||||
|
||||
writer->CloseObject();
|
||||
}
|
||||
|
||||
+203
-132
@@ -16,54 +16,71 @@
|
||||
|
||||
namespace dart {
|
||||
|
||||
struct StringToIntMapTraits {
|
||||
typedef char const* Key;
|
||||
typedef intptr_t Value;
|
||||
|
||||
struct Pair {
|
||||
Key key;
|
||||
Value value;
|
||||
Pair() : key(nullptr), value(-1) {}
|
||||
Pair(Key k, Value v) : key(k), value(v) {}
|
||||
};
|
||||
|
||||
static Value ValueOf(Pair pair) { return pair.value; }
|
||||
|
||||
static Key KeyOf(Pair pair) { return pair.key; }
|
||||
|
||||
static uword Hash(Key key) { return String::Hash(key, strlen(key)); }
|
||||
|
||||
static bool IsKeyEqual(Pair x, Key y) { return strcmp(x.key, y) == 0; }
|
||||
enum class IdSpace : uint8_t {
|
||||
kInvalid = 0, // So default-constructed ObjectIds are invalid.
|
||||
kSnapshot = 1, // Can be VM or Isolate heap, they share ids.
|
||||
kVmText = 2,
|
||||
kIsolateText = 3,
|
||||
kVmData = 4,
|
||||
kIsolateData = 5,
|
||||
kArtificial = 6, // Artificial objects (e.g. the global root).
|
||||
// Change ObjectId::kIdSpaceBits to use last entry if more are added.
|
||||
};
|
||||
|
||||
class V8SnapshotProfileWriter : public ZoneAllocated {
|
||||
public:
|
||||
enum IdSpace {
|
||||
kSnapshot = 0, // Can be VM or Isolate heap, they share ids.
|
||||
kVmText = 1,
|
||||
kIsolateText = 2,
|
||||
kVmData = 3,
|
||||
kIsolateData = 4,
|
||||
kArtificial = 5, // Artificial objects (e.g. the global root).
|
||||
kIdSpaceBits = 3,
|
||||
struct ObjectId {
|
||||
ObjectId() : ObjectId(IdSpace::kInvalid, -1) {}
|
||||
ObjectId(IdSpace space, int64_t nonce)
|
||||
: encoded_((nonce << kIdSpaceBits) | static_cast<intptr_t>(space)) {
|
||||
ASSERT(Utils::IsInt(kBitsPerInt64 - kIdSpaceBits, nonce));
|
||||
}
|
||||
|
||||
inline bool operator!=(const ObjectId& other) const {
|
||||
return encoded_ != other.encoded_;
|
||||
}
|
||||
inline bool operator==(const ObjectId& other) const {
|
||||
return !(*this != other);
|
||||
}
|
||||
|
||||
inline uword Hash() const { return Utils::WordHash(encoded_); }
|
||||
inline int64_t nonce() const { return encoded_ >> kIdSpaceBits; }
|
||||
inline IdSpace space() const {
|
||||
return static_cast<IdSpace>(encoded_ & kIdSpaceMask);
|
||||
}
|
||||
inline bool IsArtificial() const { return space() == IdSpace::kArtificial; }
|
||||
|
||||
const char* ToCString(Zone* zone) const;
|
||||
void Write(JSONWriter* writer, const char* property = nullptr) const;
|
||||
void WriteDebug(JSONWriter* writer, const char* property = nullptr) const;
|
||||
|
||||
private:
|
||||
static constexpr size_t kIdSpaceBits =
|
||||
Utils::BitLength(static_cast<int64_t>(IdSpace::kArtificial));
|
||||
static constexpr int64_t kIdSpaceMask = Utils::NBitMaskUnsafe(kIdSpaceBits);
|
||||
static const char* IdSpaceToCString(IdSpace space);
|
||||
|
||||
int64_t encoded_;
|
||||
};
|
||||
|
||||
typedef std::pair<IdSpace, intptr_t> ObjectId;
|
||||
|
||||
struct Reference {
|
||||
enum Type {
|
||||
enum class Type {
|
||||
kElement,
|
||||
kProperty,
|
||||
} reference_type;
|
||||
} type;
|
||||
union {
|
||||
intptr_t offset; // kElement
|
||||
const char* name; // kProperty
|
||||
};
|
||||
};
|
||||
|
||||
enum ConstantStrings {
|
||||
kUnknownString = 0,
|
||||
kArtificialRootString = 1,
|
||||
static Reference Element(intptr_t offset) {
|
||||
return {Type::kElement, {.offset = offset}};
|
||||
}
|
||||
static Reference Property(const char* name) {
|
||||
return {Type::kProperty, {.name = name}};
|
||||
}
|
||||
|
||||
bool IsElement() const { return type == Type::kElement; }
|
||||
};
|
||||
|
||||
static const ObjectId kArtificialRootId;
|
||||
@@ -72,47 +89,41 @@ class V8SnapshotProfileWriter : public ZoneAllocated {
|
||||
explicit V8SnapshotProfileWriter(Zone* zone) {}
|
||||
virtual ~V8SnapshotProfileWriter() {}
|
||||
|
||||
void SetObjectType(ObjectId object_id, const char* type) {}
|
||||
void SetObjectTypeAndName(ObjectId object_id,
|
||||
void SetObjectTypeAndName(const ObjectId& object_id,
|
||||
const char* type,
|
||||
const char* name) {}
|
||||
void AttributeBytesTo(ObjectId object_id, size_t num_bytes) {}
|
||||
void AttributeReferenceTo(ObjectId from_object_id,
|
||||
Reference reference,
|
||||
ObjectId to_object_id) {}
|
||||
void AttributeWeakReferenceTo(
|
||||
ObjectId from_object_id,
|
||||
Reference reference,
|
||||
ObjectId to_object_id,
|
||||
ObjectId replacement_object_id = kArtificialRootId) {}
|
||||
void AddRoot(ObjectId object_id, const char* name = nullptr) {}
|
||||
void AttributeBytesTo(const ObjectId& object_id, size_t num_bytes) {}
|
||||
void AttributeReferenceTo(const ObjectId& from_object_id,
|
||||
const Reference& reference,
|
||||
const ObjectId& to_object_id) {}
|
||||
void AttributeWeakReferenceTo(const ObjectId& from_object_id,
|
||||
const Reference& reference,
|
||||
const ObjectId& to_object_id,
|
||||
const ObjectId& replacement_object_id) {}
|
||||
void AddRoot(const ObjectId& object_id, const char* name = nullptr) {}
|
||||
bool HasId(const ObjectId& object_id) { return false; }
|
||||
#else
|
||||
explicit V8SnapshotProfileWriter(Zone* zone);
|
||||
virtual ~V8SnapshotProfileWriter() {}
|
||||
|
||||
void SetObjectType(ObjectId object_id, const char* type) {
|
||||
SetObjectTypeAndName(object_id, type, nullptr);
|
||||
}
|
||||
|
||||
// Records that the object referenced by 'object_id' has type 'type'. The
|
||||
// 'type' for all 'Instance's should be 'Instance', not the user-visible type
|
||||
// and use 'name' for the real type instead.
|
||||
void SetObjectTypeAndName(ObjectId object_id,
|
||||
void SetObjectTypeAndName(const ObjectId& object_id,
|
||||
const char* type,
|
||||
const char* name);
|
||||
|
||||
// Charges 'num_bytes'-many bytes to 'object_id'. In a clustered snapshot,
|
||||
// objects can have their data spread across multiple sections, so this can be
|
||||
// called multiple times for the same object.
|
||||
void AttributeBytesTo(ObjectId object_id, size_t num_bytes);
|
||||
void AttributeBytesTo(const ObjectId& object_id, size_t num_bytes);
|
||||
|
||||
// Records that a reference to the object with id 'to_object_id' was written
|
||||
// in order to serialize the object with id 'from_object_id'. This does not
|
||||
// affect the number of bytes charged to 'from_object_id'.
|
||||
void AttributeReferenceTo(ObjectId from_object_id,
|
||||
Reference reference,
|
||||
ObjectId to_object_id);
|
||||
void AttributeReferenceTo(const ObjectId& from_object_id,
|
||||
const Reference& reference,
|
||||
const ObjectId& to_object_id);
|
||||
|
||||
// Records that a weak serialization reference to a dropped object
|
||||
// with id 'to_object_id' was written in order to serialize the object with id
|
||||
@@ -120,14 +131,14 @@ class V8SnapshotProfileWriter : public ZoneAllocated {
|
||||
// 'replacement_object_id' is recorded as the replacement for the
|
||||
// dropped object in the snapshot. This does not affect the number of
|
||||
// bytes charged to 'from_object_id'.
|
||||
void AttributeDroppedReferenceTo(ObjectId from_object_id,
|
||||
Reference reference,
|
||||
ObjectId to_object_id,
|
||||
ObjectId replacement_object_id);
|
||||
void AttributeDroppedReferenceTo(const ObjectId& from_object_id,
|
||||
const Reference& reference,
|
||||
const ObjectId& to_object_id,
|
||||
const ObjectId& replacement_object_id);
|
||||
|
||||
// Marks an object as being a root in the graph. Used for analysis of the
|
||||
// graph.
|
||||
void AddRoot(ObjectId object_id, const char* name = nullptr);
|
||||
// Marks an object as being a root in the graph. Used for analysis of
|
||||
// the graph.
|
||||
void AddRoot(const ObjectId& object_id, const char* name = nullptr);
|
||||
|
||||
// Write to a file in the V8 Snapshot Profile (JSON/.heapsnapshot) format.
|
||||
void Write(const char* file);
|
||||
@@ -137,17 +148,61 @@ class V8SnapshotProfileWriter : public ZoneAllocated {
|
||||
bool HasId(const ObjectId& object_id);
|
||||
|
||||
private:
|
||||
static constexpr intptr_t kInvalidString =
|
||||
CStringIntMapKeyValueTrait::kNoValue;
|
||||
static constexpr intptr_t kNumNodeFields = 5;
|
||||
static constexpr intptr_t kNumEdgeFields = 3;
|
||||
|
||||
using Edge = std::pair<intptr_t, intptr_t>;
|
||||
struct Edge {
|
||||
enum class Type : intptr_t {
|
||||
kInvalid = -1,
|
||||
kContext = 0,
|
||||
kElement = 1,
|
||||
kProperty = 2,
|
||||
kInternal = 3,
|
||||
kHidden = 4,
|
||||
kShortcut = 5,
|
||||
kWeak = 6,
|
||||
kExtra = 7,
|
||||
};
|
||||
|
||||
Edge() : Edge(nullptr, Type::kInvalid, -1) {}
|
||||
Edge(V8SnapshotProfileWriter* profile_writer, const Reference& reference)
|
||||
: Edge(profile_writer,
|
||||
reference.type == Reference::Type::kElement ? Type::kElement
|
||||
: Type::kProperty,
|
||||
reference.type == Reference::Type::kElement
|
||||
? reference.offset
|
||||
: profile_writer->strings_.Add(reference.name)) {}
|
||||
Edge(V8SnapshotProfileWriter* profile_writer,
|
||||
Type type,
|
||||
intptr_t name_or_offset)
|
||||
: type(type),
|
||||
name_or_offset(name_or_offset),
|
||||
profile_writer_(profile_writer) {}
|
||||
|
||||
inline bool operator!=(const Edge& other) {
|
||||
return profile_writer_ != other.profile_writer_ || type != other.type ||
|
||||
name_or_offset != other.name_or_offset;
|
||||
}
|
||||
inline bool operator==(const Edge& other) { return !(*this != other); }
|
||||
|
||||
void Write(JSONWriter* writer, const ObjectId& target_id) const;
|
||||
void WriteDebug(JSONWriter* writer, const ObjectId& target_id) const;
|
||||
|
||||
Type type;
|
||||
intptr_t name_or_offset;
|
||||
|
||||
private:
|
||||
V8SnapshotProfileWriter* profile_writer_;
|
||||
};
|
||||
|
||||
struct EdgeToObjectIdMapTrait {
|
||||
using Key = Edge;
|
||||
using Value = ObjectId;
|
||||
|
||||
struct Pair {
|
||||
Pair() : edge{kContext, -1}, target(kArtificialRootId) {}
|
||||
Pair() : edge{}, target(kArtificialRootId) {}
|
||||
Pair(Key key, Value value) : edge(key), target(value) {}
|
||||
Edge edge;
|
||||
ObjectId target;
|
||||
@@ -156,73 +211,90 @@ class V8SnapshotProfileWriter : public ZoneAllocated {
|
||||
static Key KeyOf(Pair kv) { return kv.edge; }
|
||||
static Value ValueOf(Pair kv) { return kv.target; }
|
||||
static uword Hash(Key key) {
|
||||
return FinalizeHash(CombineHashes(key.first, key.second), 30);
|
||||
return FinalizeHash(
|
||||
CombineHashes(static_cast<intptr_t>(key.type), key.name_or_offset));
|
||||
}
|
||||
static bool IsKeyEqual(Pair kv, Key key) { return kv.edge == key; }
|
||||
};
|
||||
|
||||
using EdgeMap = ZoneDirectChainedHashMap<EdgeToObjectIdMapTrait>;
|
||||
struct EdgeMap : public ZoneDirectChainedHashMap<EdgeToObjectIdMapTrait> {
|
||||
explicit EdgeMap(Zone* zone)
|
||||
: ZoneDirectChainedHashMap<EdgeToObjectIdMapTrait>(zone) {}
|
||||
|
||||
const char* ToCString(Zone* zone) const;
|
||||
void WriteDebug(JSONWriter* writer, const char* property = nullptr) const;
|
||||
};
|
||||
|
||||
struct NodeInfo {
|
||||
intptr_t type = 0;
|
||||
intptr_t name = 0;
|
||||
ObjectId id;
|
||||
intptr_t self_size = 0;
|
||||
EdgeMap* edges = nullptr;
|
||||
// Populated during serialization.
|
||||
intptr_t offset = -1;
|
||||
// 'trace_node_id' isn't supported.
|
||||
// 'edge_count' is computed on-demand.
|
||||
NodeInfo() {}
|
||||
NodeInfo(V8SnapshotProfileWriter* profile_writer,
|
||||
const ObjectId& id,
|
||||
intptr_t type = kInvalidString,
|
||||
intptr_t name = kInvalidString)
|
||||
: id(id),
|
||||
type(type),
|
||||
name(name),
|
||||
edges(new (profile_writer->zone_) EdgeMap(profile_writer->zone_)),
|
||||
profile_writer_(profile_writer) {}
|
||||
|
||||
// Used for testing sentinel in the hashtable.
|
||||
bool operator!=(const NodeInfo& other) { return id != other.id; }
|
||||
bool operator==(const NodeInfo& other) { return !(*this != other); }
|
||||
inline bool operator!=(const NodeInfo& other) {
|
||||
return id != other.id || type != other.type || name != other.name ||
|
||||
self_size != other.self_size || edges != other.edges ||
|
||||
offset_ != other.offset_ ||
|
||||
profile_writer_ != other.profile_writer_;
|
||||
}
|
||||
inline bool operator==(const NodeInfo& other) { return !(*this != other); }
|
||||
|
||||
void AddEdge(const Edge& edge, const ObjectId& target) {
|
||||
edges->Insert({edge, target});
|
||||
}
|
||||
bool HasEdge(const Edge& edge) { return edges->HasKey(edge); }
|
||||
|
||||
// To allow NodeInfo to be used as the pair in ObjectIdToNodeInfoTraits.
|
||||
NodeInfo() : id{kSnapshot, -1} {}
|
||||
const char* ToCString(Zone* zone) const;
|
||||
void Write(JSONWriter* writer) const;
|
||||
void WriteDebug(JSONWriter* writer) const;
|
||||
|
||||
NodeInfo(Zone* zone,
|
||||
intptr_t type,
|
||||
intptr_t name,
|
||||
const ObjectId& id,
|
||||
intptr_t self_size,
|
||||
intptr_t offset)
|
||||
: type(type),
|
||||
name(name),
|
||||
id(id),
|
||||
self_size(self_size),
|
||||
edges(new (zone) EdgeMap(zone)),
|
||||
offset(offset) {}
|
||||
intptr_t offset() const { return offset_; }
|
||||
void set_offset(intptr_t offset) {
|
||||
ASSERT_EQUAL(offset_, -1);
|
||||
offset_ = offset;
|
||||
}
|
||||
|
||||
ObjectId id;
|
||||
intptr_t type = kInvalidString;
|
||||
intptr_t name = kInvalidString;
|
||||
intptr_t self_size = 0;
|
||||
EdgeMap* edges = nullptr;
|
||||
|
||||
private:
|
||||
// Populated during serialization.
|
||||
intptr_t offset_ = -1;
|
||||
// 'trace_node_id' isn't supported.
|
||||
// 'edge_count' is computed on-demand.
|
||||
|
||||
// Used for debugging prints and creating default names if none given.
|
||||
V8SnapshotProfileWriter* profile_writer_ = nullptr;
|
||||
};
|
||||
|
||||
NodeInfo* EnsureId(ObjectId object_id);
|
||||
static intptr_t NodeIdFor(ObjectId id) {
|
||||
return (id.second << kIdSpaceBits) | id.first;
|
||||
}
|
||||
NodeInfo* EnsureId(const ObjectId& object_id);
|
||||
void Write(JSONWriter* writer);
|
||||
|
||||
intptr_t EnsureString(const char* str);
|
||||
// Class that encapsulates both an array of strings and a mapping from
|
||||
// strings to their index in the array.
|
||||
class StringsTable {
|
||||
public:
|
||||
explicit StringsTable(Zone* zone)
|
||||
: zone_(zone), index_map_(zone), strings_(zone, 2) {}
|
||||
|
||||
enum ConstantEdgeType {
|
||||
kContext = 0,
|
||||
kElement = 1,
|
||||
kProperty = 2,
|
||||
kInternal = 3,
|
||||
kHidden = 4,
|
||||
kShortcut = 5,
|
||||
kWeak = 6,
|
||||
kExtra = 7,
|
||||
};
|
||||
intptr_t Add(const char* str);
|
||||
intptr_t AddFormatted(const char* fmt, ...) PRINTF_ATTRIBUTE(2, 3);
|
||||
const char* At(intptr_t index) const;
|
||||
void Write(JSONWriter* writer, const char* property = nullptr) const;
|
||||
|
||||
static ConstantEdgeType ReferenceTypeToEdgeType(Reference::Type type);
|
||||
|
||||
enum ConstantNodeType {
|
||||
kUnknown = 0,
|
||||
kArtificialRoot = 1,
|
||||
private:
|
||||
Zone* zone_;
|
||||
CStringIntMap index_map_;
|
||||
GrowableArray<const char*> strings_;
|
||||
};
|
||||
|
||||
struct ObjectIdToNodeInfoTraits {
|
||||
@@ -234,31 +306,30 @@ class V8SnapshotProfileWriter : public ZoneAllocated {
|
||||
|
||||
static Value ValueOf(const Pair& pair) { return pair; }
|
||||
|
||||
static uword Hash(Key key) { return Utils::WordHash(NodeIdFor(key)); }
|
||||
static uword Hash(const Key& key) { return key.Hash(); }
|
||||
|
||||
static bool IsKeyEqual(const Pair& x, Key y) { return x.id == y; }
|
||||
static bool IsKeyEqual(const Pair& x, const Key& y) { return x.id == y; }
|
||||
};
|
||||
|
||||
struct ObjectIdSetKeyValueTrait {
|
||||
using Pair = ObjectId;
|
||||
using Key = Pair;
|
||||
using Value = Pair;
|
||||
|
||||
static Key KeyOf(const Pair& pair) { return pair; }
|
||||
static Value ValueOf(const Pair& pair) { return pair; }
|
||||
static uword Hash(const Key& key) { return key.Hash(); }
|
||||
static bool IsKeyEqual(const Pair& pair, const Key& key) {
|
||||
return pair == key;
|
||||
}
|
||||
};
|
||||
|
||||
Zone* zone_;
|
||||
void Write(JSONWriter* writer);
|
||||
intptr_t WriteNodeInfo(JSONWriter* writer, const NodeInfo& info);
|
||||
void WriteEdgeInfo(JSONWriter* writer,
|
||||
const Edge& info,
|
||||
const ObjectId& target);
|
||||
void WriteStringsTable(JSONWriter* writer,
|
||||
const DirectChainedHashMap<StringToIntMapTraits>& map);
|
||||
|
||||
DirectChainedHashMap<ObjectIdToNodeInfoTraits> nodes_;
|
||||
DirectChainedHashMap<StringToIntMapTraits> node_types_;
|
||||
DirectChainedHashMap<StringToIntMapTraits> edge_types_;
|
||||
DirectChainedHashMap<StringToIntMapTraits> strings_;
|
||||
|
||||
// We don't have a zone-allocated hash set, so we just re-use the type for
|
||||
// nodes_ even though we don't need to access the node info (and fill it with
|
||||
// dummy values).
|
||||
DirectChainedHashMap<ObjectIdToNodeInfoTraits> roots_;
|
||||
|
||||
size_t edge_count_ = 0;
|
||||
StringsTable node_types_;
|
||||
StringsTable edge_types_;
|
||||
StringsTable strings_;
|
||||
DirectChainedHashMap<ObjectIdSetKeyValueTrait> roots_;
|
||||
#endif
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user