[vm] Add --resolve-dwarf-paths (disabled by default).

When --resolve-dwarf-paths is enabled, then paths output to DWARF
information will be resolved to either an absolute or relative path.
If this cannot be done, snapshot creation fails.

File URIs are output as absolute paths.

SDK URIs are output as paths relative to the SDK root.

TEST=vm/dart{,_2}/use_resolve_dwarf_paths_flag_test

Cq-Include-Trybots: luci.dart.try:vm-kernel-precomp-dwarf-linux-product-x64-try,vm-kernel-precomp-linux-debug-x64-try,vm-kernel-precomp-linux-release-x64-try,vm-kernel-precomp-nnbd-linux-debug-x64-try,vm-kernel-precomp-nnbd-linux-release-x64-try,vm-kernel-precomp-linux-product-x64-try
Change-Id: I63c694f0f707ef6a3d3faa690e001fefe2b26094
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/196491
Commit-Queue: Tess Strickland <sstrickl@google.com>
Reviewed-by: Daco Harkes <dacoharkes@google.com>
diff --git a/pkg/expect/lib/expect.dart b/pkg/expect/lib/expect.dart
index 9ad6e5b..59b0c0d 100644
--- a/pkg/expect/lib/expect.dart
+++ b/pkg/expect/lib/expect.dart
@@ -178,6 +178,15 @@
   }
 
   /**
+   * Checks whether the Iterable [actual] is not empty.
+   */
+  static void isNotEmpty(Iterable actual, [String reason = ""]) {
+    if (actual.isNotEmpty) return;
+    String msg = _getMessage(reason);
+    _fail("Expect.isNotEmpty(actual: <$actual>$msg) fails.");
+  }
+
+  /**
    * Checks whether the expected and actual values are identical
    * (using `identical`).
    */
diff --git a/runtime/tests/vm/dart/use_resolve_dwarf_paths_flag_test.dart b/runtime/tests/vm/dart/use_resolve_dwarf_paths_flag_test.dart
new file mode 100644
index 0000000..30d90d5
--- /dev/null
+++ b/runtime/tests/vm/dart/use_resolve_dwarf_paths_flag_test.dart
@@ -0,0 +1,102 @@
+// Copyright (c) 2021, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+// This test checks that --resolve-dwarf-paths outputs absolute and relative
+// paths in DWARF information.
+
+// OtherResources=use_dwarf_stack_traces_flag_program.dart
+
+import "dart:async";
+import "dart:io";
+
+import 'package:expect/expect.dart';
+import 'package:native_stack_traces/native_stack_traces.dart';
+import 'package:path/path.dart' as path;
+
+import 'use_flag_test_helper.dart';
+
+main(List<String> args) async {
+  if (!isAOTRuntime) {
+    return; // Running in JIT: AOT binaries not available.
+  }
+
+  if (Platform.isAndroid) {
+    return; // SDK tree and dart_bootstrap not available on the test device.
+  }
+
+  // These are the tools we need to be available to run on a given platform:
+  if (!await testExecutable(genSnapshot)) {
+    throw "Cannot run test as $genSnapshot not available";
+  }
+  if (!await testExecutable(aotRuntime)) {
+    throw "Cannot run test as $aotRuntime not available";
+  }
+  if (!File(platformDill).existsSync()) {
+    throw "Cannot run test as $platformDill does not exist";
+  }
+
+  await withTempDir('dwarf-flag-test', (String tempDir) async {
+    final cwDir = path.dirname(Platform.script.toFilePath());
+    final script = path.join(cwDir, 'use_dwarf_stack_traces_flag_program.dart');
+    final scriptDill = path.join(tempDir, 'flag_program.dill');
+
+    // Compile script to Kernel IR.
+    await run(genKernel, <String>[
+      '--aot',
+      '--platform=$platformDill',
+      '-o',
+      scriptDill,
+      script,
+    ]);
+
+    final scriptDwarfSnapshot = path.join(tempDir, 'dwarf.so');
+    await run(genSnapshot, <String>[
+      '--resolve-dwarf-paths',
+      '--dwarf-stack-traces-mode',
+      '--snapshot-kind=app-aot-elf',
+      '--elf=$scriptDwarfSnapshot',
+      scriptDill,
+    ]);
+
+    // Run the resulting Dwarf-AOT compiled script.
+    final dwarfTrace = await runError(aotRuntime, <String>[
+      scriptDwarfSnapshot,
+      scriptDill,
+    ]);
+
+    final tracePCOffsets = collectPCOffsets(dwarfTrace);
+
+    // Check that translating the DWARF stack trace (without internal frames)
+    // matches the symbolic stack trace.
+    final dwarf = Dwarf.fromFile(scriptDwarfSnapshot);
+    Expect.isNotNull(dwarf);
+    checkDwarfInfo(dwarf!, tracePCOffsets);
+  });
+}
+
+void checkDwarfInfo(Dwarf dwarf, Iterable<PCOffset> offsets) {
+  final filenames = <String>{};
+  for (final offset in offsets) {
+    final callInfo = offset.callInfoFrom(dwarf);
+    Expect.isNotNull(callInfo);
+    Expect.isNotEmpty(callInfo!);
+    for (final e in callInfo) {
+      Expect.isTrue(e is DartCallInfo, 'Call is not from the Dart source: $e.');
+      final entry = e as DartCallInfo;
+      var filename = entry.filename;
+      if (!filename.startsWith('/')) {
+        filename = path.join(sdkDir, filename);
+      }
+      if (filenames.add(filename)) {
+        Expect.isTrue(
+            File(filename).existsSync(), 'File $filename does not exist.');
+      }
+    }
+  }
+  print('Checked filenames:');
+  for (final filename in filenames) {
+    print('- ${filename}');
+  }
+  Expect.isNotEmpty(filenames);
+}
diff --git a/runtime/tests/vm/dart_2/use_resolve_dwarf_paths_flag_test.dart b/runtime/tests/vm/dart_2/use_resolve_dwarf_paths_flag_test.dart
new file mode 100644
index 0000000..8b00996
--- /dev/null
+++ b/runtime/tests/vm/dart_2/use_resolve_dwarf_paths_flag_test.dart
@@ -0,0 +1,102 @@
+// Copyright (c) 2021, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+// This test checks that --resolve-dwarf-paths outputs absolute and relative
+// paths in DWARF information.
+
+// OtherResources=use_dwarf_stack_traces_flag_program.dart
+
+import "dart:async";
+import "dart:io";
+
+import 'package:expect/expect.dart';
+import 'package:native_stack_traces/native_stack_traces.dart';
+import 'package:path/path.dart' as path;
+
+import 'use_flag_test_helper.dart';
+
+main(List<String> args) async {
+  if (!isAOTRuntime) {
+    return; // Running in JIT: AOT binaries not available.
+  }
+
+  if (Platform.isAndroid) {
+    return; // SDK tree and dart_bootstrap not available on the test device.
+  }
+
+  // These are the tools we need to be available to run on a given platform:
+  if (!await testExecutable(genSnapshot)) {
+    throw "Cannot run test as $genSnapshot not available";
+  }
+  if (!await testExecutable(aotRuntime)) {
+    throw "Cannot run test as $aotRuntime not available";
+  }
+  if (!File(platformDill).existsSync()) {
+    throw "Cannot run test as $platformDill does not exist";
+  }
+
+  await withTempDir('dwarf-flag-test', (String tempDir) async {
+    final cwDir = path.dirname(Platform.script.toFilePath());
+    final script = path.join(cwDir, 'use_dwarf_stack_traces_flag_program.dart');
+    final scriptDill = path.join(tempDir, 'flag_program.dill');
+
+    // Compile script to Kernel IR.
+    await run(genKernel, <String>[
+      '--aot',
+      '--platform=$platformDill',
+      '-o',
+      scriptDill,
+      script,
+    ]);
+
+    final scriptDwarfSnapshot = path.join(tempDir, 'dwarf.so');
+    await run(genSnapshot, <String>[
+      '--resolve-dwarf-paths',
+      '--dwarf-stack-traces-mode',
+      '--snapshot-kind=app-aot-elf',
+      '--elf=$scriptDwarfSnapshot',
+      scriptDill,
+    ]);
+
+    // Run the resulting Dwarf-AOT compiled script.
+    final dwarfTrace = await runError(aotRuntime, <String>[
+      scriptDwarfSnapshot,
+      scriptDill,
+    ]);
+
+    final tracePCOffsets = collectPCOffsets(dwarfTrace);
+
+    // Check that translating the DWARF stack trace (without internal frames)
+    // matches the symbolic stack trace.
+    final dwarf = Dwarf.fromFile(scriptDwarfSnapshot);
+    Expect.isNotNull(dwarf);
+    checkDwarfInfo(dwarf, tracePCOffsets);
+  });
+}
+
+void checkDwarfInfo(Dwarf dwarf, Iterable<PCOffset> offsets) {
+  final filenames = <String>{};
+  for (final offset in offsets) {
+    final callInfo = offset.callInfoFrom(dwarf);
+    Expect.isNotNull(callInfo);
+    Expect.isNotEmpty(callInfo);
+    for (final e in callInfo) {
+      Expect.isTrue(e is DartCallInfo, 'Call is not from the Dart source: $e.');
+      final entry = e as DartCallInfo;
+      var filename = entry.filename;
+      if (!filename.startsWith('/')) {
+        filename = path.join(sdkDir, filename);
+      }
+      if (filenames.add(filename)) {
+        Expect.isTrue(
+            File(filename).existsSync(), 'File $filename does not exist.');
+      }
+    }
+  }
+  print('Checked filenames:');
+  for (final filename in filenames) {
+    print('- ${filename}');
+  }
+  Expect.isNotEmpty(filenames);
+}
diff --git a/runtime/vm/compiler/aot/precompiler.cc b/runtime/vm/compiler/aot/precompiler.cc
index 0ad8406..9107586 100644
--- a/runtime/vm/compiler/aot/precompiler.cc
+++ b/runtime/vm/compiler/aot/precompiler.cc
@@ -2492,6 +2492,10 @@
   KernelProgramInfo& program_info = KernelProgramInfo::Handle(Z);
   const TypedData& null_typed_data = TypedData::Handle(Z);
   const KernelProgramInfo& null_info = KernelProgramInfo::Handle(Z);
+#if defined(PRODUCT)
+  auto& str = String::Handle(Z);
+  auto& wsr = WeakSerializationReference::Handle(Z);
+#endif
 
   for (intptr_t i = 0; i < libraries_.Length(); i++) {
     lib ^= libraries_.At(i);
@@ -2538,7 +2542,11 @@
           program_info.set_classes_cache(Array::null_array());
         }
 #if defined(PRODUCT)
-        script.set_resolved_url(String::null_string());
+        str = script.resolved_url();
+        if (!str.IsNull()) {
+          wsr = WeakSerializationReference::New(str, String::null_string());
+          script.set_resolved_url(wsr);
+        }
 #endif  // defined(PRODUCT)
         script.set_compile_time_constants(Array::null_array());
         script.set_line_starts(null_typed_data);
diff --git a/runtime/vm/dwarf.cc b/runtime/vm/dwarf.cc
index 35df763..f940ef7 100644
--- a/runtime/vm/dwarf.cc
+++ b/runtime/vm/dwarf.cc
@@ -14,6 +14,11 @@
 
 #if defined(DART_PRECOMPILER)
 
+DEFINE_FLAG(bool,
+            resolve_dwarf_paths,
+            false,
+            "Resolve script URIs to absolute or relative file paths in DWARF");
+
 DEFINE_FLAG(charp,
             write_code_comments_as_synthetic_source_to,
             nullptr,
@@ -718,6 +723,25 @@
   }
 }
 
+static constexpr char kResolvedFileRoot[] = "file://";
+static constexpr intptr_t kResolvedFileRootLen = sizeof(kResolvedFileRoot) - 1;
+static constexpr char kResolvedSdkRoot[] = "org-dartlang-sdk:///sdk/";
+static constexpr intptr_t kResolvedSdkRootLen = sizeof(kResolvedSdkRoot) - 1;
+
+static const char* ConvertResolvedURI(const char* str) {
+  const intptr_t len = strlen(str);
+  if (len > kResolvedFileRootLen &&
+      strncmp(str, kResolvedFileRoot, kResolvedFileRootLen) == 0) {
+    return str + kResolvedFileRootLen;
+  }
+  if (len > kResolvedSdkRootLen &&
+      strncmp(str, kResolvedSdkRoot, kResolvedSdkRootLen) == 0) {
+    // Leave "sdk/" as a prefix in the returned path.
+    return str + (kResolvedSdkRootLen - 4);
+  }
+  return nullptr;
+}
+
 void Dwarf::WriteLineNumberProgram(DwarfWriteStream* stream) {
   // 6.2.4 The Line Number Program Header
 
@@ -762,8 +786,25 @@
   String& uri = String::Handle(zone_);
   for (intptr_t i = 0; i < scripts_.length(); i++) {
     const Script& script = *(scripts_[i]);
-    uri = script.url();
-    auto const uri_cstr = Deobfuscate(uri.ToCString());
+    if (FLAG_resolve_dwarf_paths) {
+      uri = script.resolved_url();
+      // Strictly enforce this to catch unresolvable cases.
+      if (uri.IsNull()) {
+        FATAL("no resolved URI for Script %s available", script.ToCString());
+      }
+    } else {
+      uri = script.url();
+    }
+    ASSERT(!uri.IsNull());
+    auto uri_cstr = Deobfuscate(uri.ToCString());
+    if (FLAG_resolve_dwarf_paths) {
+      auto const converted_cstr = ConvertResolvedURI(uri_cstr);
+      // Strictly enforce this to catch inconvertable cases.
+      if (converted_cstr == nullptr) {
+        FATAL("cannot convert resolved URI %s", uri_cstr);
+      }
+      uri_cstr = converted_cstr;
+    }
     RELEASE_ASSERT(strlen(uri_cstr) != 0);
 
     stream->string(uri_cstr);  // NOLINT
diff --git a/runtime/vm/object.cc b/runtime/vm/object.cc
index e163b57..1d9e3ce 100644
--- a/runtime/vm/object.cc
+++ b/runtime/vm/object.cc
@@ -11392,6 +11392,15 @@
 }
 #endif
 
+StringPtr Script::resolved_url() const {
+#if defined(DART_PRECOMPILER)
+  return String::RawCast(
+      WeakSerializationReference::Unwrap(untag()->resolved_url()));
+#else
+  return untag()->resolved_url();
+#endif
+}
+
 bool Script::HasSource() const {
   return untag()->source() != String::null();
 }
@@ -11546,9 +11555,15 @@
   untag()->set_url(value.ptr());
 }
 
+#if defined(DART_PRECOMPILER)
+void Script::set_resolved_url(const Object& value) const {
+  untag()->set_resolved_url(value.ptr());
+}
+#else
 void Script::set_resolved_url(const String& value) const {
   untag()->set_resolved_url(value.ptr());
 }
+#endif
 
 void Script::set_source(const String& value) const {
   untag()->set_source(value.ptr());
diff --git a/runtime/vm/object.h b/runtime/vm/object.h
index 9ddb727..c39a387 100644
--- a/runtime/vm/object.h
+++ b/runtime/vm/object.h
@@ -4426,7 +4426,7 @@
   void set_url(const String& value) const;
 
   // The actual url which was loaded from disk, if provided by the embedder.
-  StringPtr resolved_url() const { return untag()->resolved_url(); }
+  StringPtr resolved_url() const;
   bool HasSource() const;
   StringPtr Source() const;
   bool IsPartOfDartColonLibrary() const;
@@ -4526,7 +4526,11 @@
   void SetCachedMaxPosition(intptr_t value) const;
 #endif  // !defined(DART_PRECOMPILED_RUNTIME)
 
+#if defined(DART_PRECOMPILER)
+  void set_resolved_url(const Object& value) const;
+#else
   void set_resolved_url(const String& value) const;
+#endif
   void set_source(const String& value) const;
   void set_load_timestamp(int64_t value) const;
   ArrayPtr debug_positions() const;
diff --git a/runtime/vm/raw_object.h b/runtime/vm/raw_object.h
index a50a643..f386161 100644
--- a/runtime/vm/raw_object.h
+++ b/runtime/vm/raw_object.h
@@ -1486,7 +1486,13 @@
 
   VISIT_FROM(CompressedObjectPtr, url)
   COMPRESSED_POINTER_FIELD(StringPtr, url)
+#if defined(DART_PRECOMPILER)
+  // We use WSRs to keep the information available to the DWARF writer
+  // without being actually serialized.
+  COMPRESSED_POINTER_FIELD(ObjectPtr, resolved_url)
+#else
   COMPRESSED_POINTER_FIELD(StringPtr, resolved_url)
+#endif
   COMPRESSED_POINTER_FIELD(ArrayPtr, compile_time_constants)
   COMPRESSED_POINTER_FIELD(TypedDataPtr, line_starts)
 #if !defined(PRODUCT) && !defined(DART_PRECOMPILED_RUNTIME)