Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Rust v0 symbol mangling scheme #491

Merged
merged 21 commits into from
Jan 16, 2025
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
node_modules
.cache
.parcel-cache
dist
.idea
coverage
Expand Down
4 changes: 2 additions & 2 deletions assets/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@
</head>

<body>
<script src="../src/speedscope.tsx"></script>
<script type="module" src="../src/speedscope.tsx"></script>
</body>

</html>
</html>
42,810 changes: 22,572 additions & 20,238 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,10 @@
"jsverify": "0.8.3",
"jszip": "3.1.5",
"pako": "1.0.6",
"parcel-bundler": "1.12.4",
"parcel": "2.13.3",
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was this necessary as part of this change? I'm not opposed per-se, but I don't like upgrading dependencies as part of PRs that introduce other changes in case they need reverting

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fully understand. I added the explanation in the commit. Sadly the emscripten generated code is a raw JavaScript file that contains features that are not supported by That version of parcel-bundled hence why I had to upgrade it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is required because emscripten glue code now uses es6 features
like logicalAssignment "??=" which is only supported by newer version
of parcel.

"preact": "10.4.1",
"prettier": "3.1.1",
"process": "^0.11.10",
"protobufjs": "6.8.8",
"source-map": "0.6.1",
"ts-jest": "24.3.0",
Expand Down
2 changes: 2 additions & 0 deletions src/gl/graphics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@

// NOTE: This file intentionally has no dependencies.

declare const process: any

// Dependencies & polyfills for import from skew
const RELEASE =
typeof process !== 'undefined' && process.env && process.env.NODE_ENV === 'production'
Expand Down
13 changes: 0 additions & 13 deletions src/lib/demangle-cpp.test.ts

This file was deleted.

32 changes: 0 additions & 32 deletions src/lib/demangle-cpp.ts

This file was deleted.

1 change: 1 addition & 0 deletions src/lib/demangle/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
gcc
35 changes: 35 additions & 0 deletions src/lib/demangle/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
EMCC = emcc
CFLAGS = -Os -Igcc/include -DHAVE_STDLIB_H -DHAVE_STRING_H
LDFLAGS_COMMON = \
-s EXPORTED_RUNTIME_METHODS=stringToUTF8OnStack,UTF8ToString \
-s EXPORTED_FUNCTIONS=_demangle,_free \
-s MODULARIZE=1 \
-s WASM=1 \
-s FILESYSTEM=0

# To use inside speedscope, we have to disable EXPORT_ES6 as otherwise
# jest complain because it doesn't support es6 module export. It doesn't happen
# with .ts because they are compiled to compatible js.
ifeq ($(TEST),1)
LDFLAGS = $(LDFLAGS_COMMON) -s EXPORT_ES6=1
else
LDFLAGS = $(LDFLAGS_COMMON) -s SINGLE_FILE=1 -s ENVIRONMENT=web
endif

SRC_FILES = \
gcc/libiberty/safe-ctype.c \
gcc/libiberty/rust-demangle.c \
gcc/libiberty/cp-demangle.c \
demangle.c
POST_JS = demangle.post.js
OUTPUT = demangle.wasm.js

all: $(OUTPUT)

$(OUTPUT): $(SRC_FILES) $(POST_JS)
$(EMCC) $(CFLAGS) $(SRC_FILES) $(LDFLAGS) --post-js $(POST_JS) --no-entry -o $(OUTPUT)

clean:
rm -f $(OUTPUT)

.PHONY: all clean
28 changes: 28 additions & 0 deletions src/lib/demangle/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# demangle

A wrapper function on top of demangling functions from the `GNU libiberty`,
using emscripten.

# Build dependencies

Follow the official `emsdk` installation instructions:

https://emscripten.org/docs/getting_started/downloads.html#installation-instructions-using-the-emsdk-recommended

And make sure you have `emcc` in your PATH.

# Source dependencies

## GCC

Make sure to fetch `gcc` sources.

* `git clone https://github.com/gcc-mirror/gcc`
* `git reset --hard 40754a3b9bef83bf4da0675fcb378e8cd1675602`

# Build instructions

`make` to produce a single CommonJS module that contains also contain the base64 encoded wasm file.
`make TEST=1` to produce both a ES6 module AND the wasm file.

Using `make TEST=1` produce a file that can be used by `node` for testing purposes.
38 changes: 38 additions & 0 deletions src/lib/demangle/demangle.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#include "gcc/include/demangle.h"

#include <string.h>

static char *non_microsoft_demangle(const char *mangled) {
int is_itanium_symbol = strncmp(mangled, "_Z", 2) == 0;
if (is_itanium_symbol) {
// Note: __cxa_demangle default is DMGL_PARAMS | DMGL_TYPES
return cplus_demangle_v3(mangled, DMGL_PARAMS | DMGL_TYPES);
}

int is_rust_symbol = strncmp(mangled, "_R", 2) == 0;
if (is_rust_symbol) {
// Note: rust_demangle uses only DMGL_VERBOSE and DMGL_NO_RECURSE_LIMIT,
// so no need to pass any options in our case.
return rust_demangle(mangled, DMGL_NO_OPTS);
}

return NULL;
}

// Logic is inspired by llvm::demangle.
// It is the caller's responsibility to free the string which is returned.
char *demangle(const char *mangled) {
char *demangled = non_microsoft_demangle(mangled);
if (demangled) {
return demangled;
}

if (mangled[0] == '_') {
demangled = non_microsoft_demangle(&mangled[1]);
if (demangled) {
return demangled;
}
}

return NULL;
}
22 changes: 22 additions & 0 deletions src/lib/demangle/demangle.post.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* DO NOT USE THIS FILE DIRECTLY.
*
* This file is only used as --post-js of emcc.
*
* This file provides a higher level demangle function ready to use
* in JavaScript.
*/
Module['wasm_demangle'] = function(mangled) {
/*
* We are manually calling the lower-level generated functions
* instead of using `cwrap` because we need to `free` the pointer
* returned by `_demangle`.
*/
const param_ptr = stringToUTF8OnStack(mangled);
const result_ptr = _demangle(param_ptr);
const result = UTF8ToString(result_ptr);
if (result_ptr !== null && result_ptr !== undefined) {
_free(result_ptr);
}
return result;
}
25 changes: 25 additions & 0 deletions src/lib/demangle/demangle.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import {loadDemangling} from './demangle'

test('demangle', async () => {
const demangle = await loadDemangling()

expect(demangle('a')).toBe('a')
expect(demangle('someUnobfuscatedFunction')).toBe('someUnobfuscatedFunction')

// C++ mangling
expect(demangle('__ZNK7Support6ColorFeqERKS0_')).toBe(
'Support::ColorF::operator==(Support::ColorF const&) const',
)
// Running a second time to test the cache
expect(demangle('__ZNK7Support6ColorFeqERKS0_')).toBe(
'Support::ColorF::operator==(Support::ColorF const&) const',
)

// Rust v0 mangling
expect(demangle('_RNvCskwGfYPst2Cb_3foo16example_function')).toBe('foo::example_function')

// Rust legacy mangling
expect(demangle('_ZN3std2fs8Metadata7created17h8df207f105c5d474E')).toBe(
'std::fs::Metadata::created::h8df207f105c5d474',
)
})
cerisier marked this conversation as resolved.
Show resolved Hide resolved
25 changes: 25 additions & 0 deletions src/lib/demangle/demangle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import createWasmDemangleModule from './demangle.wasm'

const cache = new Map<string, string>()

export async function loadDemangling(): Promise<(name: string) => string> {
// This function converts a mangled C++ name such as "__ZNK7Support6ColorFeqERKS0_"
// into a human-readable symbol (in this case "Support::ColorF::==(Support::ColorF&)")
const wasmDemangleModule = await createWasmDemangleModule()
return cached(wasmDemangleModule.wasm_demangle)
}

function cached(demangle: (name: string) => string): (name: string) => string {
return (name: string): string => {
let result = cache.get(name)
if (result !== undefined) {
name = result
} else {
result = demangle(name)
result = result === '' ? name : result
cache.set(name, result)
name = result
}
return name
}
}
5 changes: 5 additions & 0 deletions src/lib/demangle/demangle.wasm.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
interface WasmDemangleModule {
wasm_demangle(mangled: string): string
}

export default function ModuleFactory(options?: unknown): Promise<WasmDemangleModule>
22 changes: 22 additions & 0 deletions src/lib/demangle/demangle.wasm.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/lib/demangle/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {loadDemangling} from './demangle'
19 changes: 11 additions & 8 deletions src/lib/profile.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import {lastOf, KeyedSet} from './utils'
import {ValueFormatter, RawValueFormatter} from './value-formatters'
import {FileFormat} from './file-format-spec'
const demangleCppModule = import('./demangle-cpp')

export interface FrameInfo {
key: string | number
Expand Down Expand Up @@ -404,16 +403,20 @@ export class Profile {

// Demangle symbols for readability
async demangle() {
let demangleCpp: ((name: string) => string) | null = null
let demangle: ((name: string) => string) | null = null

for (let frame of this.frames) {
// This function converts a mangled C++ name such as "__ZNK7Support6ColorFeqERKS0_"
// into a human-readable symbol (in this case "Support::ColorF::==(Support::ColorF&)")
if (frame.name.startsWith('__Z')) {
if (!demangleCpp) {
demangleCpp = (await demangleCppModule).demangleCpp
// This function converts a mangled C++ and Rust name into a human-readable symbol.
if (
frame.name.startsWith('__Z') ||
frame.name.startsWith('_R') ||
frame.name.startsWith('_Z')
) {
if (!demangle) {
const demangleModule = await import('./demangle')
Copy link
Owner

@jlfwong jlfwong Jan 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The import was above intentionally at the top of the file to cause eager, non-blocking loading of this module. As written here, the load of this module will only begin after a profile import, which will be much slower.

In this case, I think we want both the import and the loadDemangling to happen eagerly, and then to await the promise in here.

I think the right way of doing that is to eagerly do the import here, and to eagerly create the wasm module in demangle.ts

Copy link
Contributor Author

@cerisier cerisier Jan 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From what I read, demangle is already imported eagerly by application.tsx no ?

As for demangle.ts, is it fine if I do this ?

diff --git a/src/lib/demangle/demangle.ts b/src/lib/demangle/demangle.ts
index b69adeb..70f17a0 100644
--- a/src/lib/demangle/demangle.ts
+++ b/src/lib/demangle/demangle.ts
@@ -1,11 +1,13 @@
 import createWasmDemangleModule from './demangle.wasm'
 
+const wasmDemangleModulePromise = createWasmDemangleModule().then((module) => module)
+
 const cache = new Map<string, string>()
 
 export async function loadDemangling(): Promise<(name: string) => string> {
   // This function converts a mangled C++ name such as "__ZNK7Support6ColorFeqERKS0_"
   // into a human-readable symbol (in this case "Support::ColorF::==(Support::ColorF&)")
-  const wasmDemangleModule = await createWasmDemangleModule()
+  const wasmDemangleModule = await wasmDemangleModulePromise
   return cached(wasmDemangleModule.wasm_demangle)
 }

That way, everything is eager from application.tsx.
import ("./demangle") from profile.tsx is awaiting the eager loaded import from application.tsx
and loadDemangling in profile.tsx just await the eager loaded wasmModule triggered by the eager loding of demangle from `application.tsx.

demangle = await demangleModule.loadDemangling()
}
frame.name = demangleCpp(frame.name)
frame.name = demangle(frame.name)
}
}
}
Expand Down
8 changes: 5 additions & 3 deletions src/views/application.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const importModule = import('../import')
// We put them all in one place so we can directly control the relative priority
// of these.
importModule.then(() => {})
import('../lib/demangle-cpp').then(() => {})
import('../lib/demangle').then(() => {})
import('source-map').then(() => {})

async function importProfilesFromText(
Expand Down Expand Up @@ -56,8 +56,10 @@ async function importFromFileSystemDirectoryEntry(entry: FileSystemDirectoryEntr
return (await importModule).importFromFileSystemDirectoryEntry(entry)
}

declare function require(x: string): any
const exampleProfileURL = require('../../sample/profiles/stackcollapse/perf-vertx-stacks-01-collapsed-all.txt')
const exampleProfileURL = new URL(
'../../sample/profiles/stackcollapse/perf-vertx-stacks-01-collapsed-all.txt',
import.meta.url,
)

function isFileSystemDirectoryEntry(entry: FileSystemEntry): entry is FileSystemDirectoryEntry {
return entry != null && entry.isDirectory
Expand Down
Loading