Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
13 changes: 12 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,20 @@ jobs:
node-version: 24
- name: Add WASM target
run: rustup target add wasm32-unknown-unknown
- name: Validate artifact dependencies
run: npm run check:dependencies
- run: npm ci --prefix packages/libdatadog
- run: cargo install wasm-pack --version 0.14.0 --locked
- run: npm run build:wasm --prefix packages/libdatadog
- run: cargo install cargo-bloat --version 0.12.1 --locked
- run: npm run build --prefix packages/libdatadog
- name: Report N-API binary size
run: npm run report:napi-size --prefix packages/libdatadog
- name: Build symbolized WASM for size attribution
run: npm run size:profile --prefix packages/libdatadog
- name: Report and validate WASM binary size
run: >-
npm run report:wasm-size --prefix packages/libdatadog --
../../target/size/libdatadog_wasm_bg.wasm
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: libdatadog-wasm
Expand Down
14 changes: 14 additions & 0 deletions crates/libdatadog-wasm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,17 @@ wasm-opt = [
"--enable-multivalue",
"--converge",
]

[package.metadata.wasm-pack.profile.profiling]
# Keep function names for crate attribution after wasm-opt runs in Linux CI.
wasm-opt = [
"-Oz",
"--enable-mutable-globals",
"--enable-nontrapping-float-to-int",
"--enable-bulk-memory",
"--enable-sign-ext",
"--enable-reference-types",
"--enable-multivalue",
"--converge",
"-g",
]
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"cargo-build-release": "yarn -s cargo-build -- --release",
"cargo-build": "cargo build --message-format=json-render-diagnostics",
"copy-artifacts": "node ./scripts/copy-artifacts",
"check:dependencies": "node scripts/check-dependencies.js",
"lint": "eslint .",
"test": "bash scripts/test.sh"
},
Expand Down
3 changes: 3 additions & 0 deletions packages/libdatadog/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,10 @@
"build:native": "node scripts/build-native.js",
"build:wasm": "npm run build:wasm:binary && npm run inline:wasm",
"build:wasm:binary": "node ../../scripts/build-wasm.js ../../crates/libdatadog-wasm wasm/dist",
"size:profile": "node ../../scripts/build-wasm.js ../../crates/libdatadog-wasm ../../target/size --profiling",
"inline:wasm": "node scripts/inline-wasm.js",
"report:napi-size": "node scripts/report-napi-size.js",
"report:wasm-size": "node scripts/report-wasm-size.js",
"package:native": "node scripts/package-native.js",
"build": "npm run build:native && npm run build:wasm",
"test": "node scripts/run-tests.js && npm run test:types",
Expand Down
183 changes: 183 additions & 0 deletions packages/libdatadog/scripts/report-napi-size.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
'use strict'

const fs = require('node:fs')
const path = require('node:path')
const { spawnSync } = require('node:child_process')

function formatBytes (bytes) {
return bytes.toLocaleString('en-US')
}

function formatKibibytes (bytes) {
return (bytes / 1024).toFixed(1)
}

function findArtifact () {
const directory = path.join(__dirname, '..', 'dist', 'native')
const artifacts = fs.readdirSync(directory)
.filter(file => /^libdatadog\..+\.node$/.test(file))

if (artifacts.length !== 1) {
throw new Error(`expected one native artifact in ${directory}, found ${artifacts.length}`)
}

return path.join(directory, artifacts[0])
}

function runCommand (command, args) {
const result = spawnSync(command, args, {
encoding: 'utf8',
maxBuffer: 10 * 1024 * 1024,
})

if (result.error) throw result.error
if (result.status !== 0) {
const detail = result.stderr.trim() || result.stdout.trim()
throw new Error(`${command} failed: ${detail}`)
}

return result.stdout
}

function parseDarwinSections (output) {
const sections = []

for (const line of output.split('\n')) {
const match = line.match(/^Segment (\S+):\s+(\d+)$/)
if (match) sections.push({ name: match[1], bytes: Number(match[2]) })
}

return sections
}

function parseElfSections (output) {
const sections = []

for (const line of output.split('\n')) {
const match = line.trim().match(/^(\S+)\s+(\d+)\s+[0-9a-fA-Fx]+$/)
if (match && match[1] !== 'section') {
sections.push({ name: match[1], bytes: Number(match[2]) })
}
}

return sections
}

function readSections (artifactPath, platform = process.platform) {
const sections = platform === 'darwin'
? parseDarwinSections(runCommand('size', ['-m', artifactPath]))
: parseElfSections(runCommand('size', ['-A', artifactPath]))

if (sections.length === 0) throw new Error('could not read native binary sections')
return sections
}

function readCrateSizes () {
const output = runCommand('cargo', [
'bloat',
'--release',
'--crates',
'-n',
'0',
'--message-format',
'json',
'-p',
'libdatadog',
'--lib',
])

return JSON.parse(output)
}

function appendCrateReport (lines, profile) {
const crates = profile.crates
.map(crate => ({ ...crate, name: crate.name.replaceAll('_', '-') }))
const attributedBytes = crates.reduce((total, crate) => total + crate.size, 0)
const overheadBytes = profile['text-section-size'] - attributedBytes
if (overheadBytes > 0) {
crates.push({ name: 'unattributed .text overhead', size: overheadBytes })
crates.sort((left, right) => right.size - left.size)
}
const visible = crates.filter(crate => crate.size >= 2048)
const otherBytes = crates
.filter(crate => crate.size < 2048)
.reduce((total, crate) => total + crate.size, 0)

if (otherBytes > 0) {
visible.push({ name: 'other crates (<2 KiB each)', size: otherBytes })
}

lines.push(
'',
'### Code by Rust crate',
'',
'| Crate | Bytes | KiB | Share of native code |',
'| --- | ---: | ---: | ---: |',
)

for (const crate of visible) {
const share = `${(crate.size / profile['text-section-size'] * 100).toFixed(1)}%`
lines.push(
`| ${crate.name} | ${formatBytes(crate.size)} | `
+ `${formatKibibytes(crate.size)} | ${share} |`,
)
}

lines.push(
'',
`.text section: ${formatBytes(profile['text-section-size'])} bytes `
+ `(${formatKibibytes(profile['text-section-size'])} KiB).`,
'',
'Crate ownership comes from cargo-bloat using a separate symbol-preserving release build. '
+ 'It attributes native code only; data, unwind information, symbols, and file alignment '
+ 'remain in the section and artifact totals above.',
)
}

function createReport (artifactPath, profile) {
const artifactBytes = fs.statSync(artifactPath).size
const sections = readSections(artifactPath)
const totalSectionBytes = sections.reduce((total, section) => total + section.bytes, 0)
const lines = [
'## libdatadog N-API size',
'',
'| Native artifact | Bytes | KiB |',
'| --- | ---: | ---: |',
`| **Shipped .node file** | **${formatBytes(artifactBytes)}** | `
+ `**${formatKibibytes(artifactBytes)}** |`,
'',
'### Native binary sections',
'',
'| Section/segment | Bytes | KiB | Share |',
'| --- | ---: | ---: | ---: |',
]

for (const section of sections) {
const share = `${(section.bytes / totalSectionBytes * 100).toFixed(1)}%`
lines.push(
`| ${section.name} | ${formatBytes(section.bytes)} | `
+ `${formatKibibytes(section.bytes)} | ${share} |`,
)
}

if (profile) appendCrateReport(lines, profile)

lines.push('', `Generated from \`${path.relative(process.cwd(), artifactPath)}\`.`)
return lines.join('\n')
}

if (require.main === module) {
const artifactPath = process.argv[2] ? path.resolve(process.argv[2]) : findArtifact()
const report = createReport(artifactPath, readCrateSizes())
console.log(report)

if (process.env.GITHUB_STEP_SUMMARY) {
fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, `${report}\n`)
}
}

module.exports = {
createReport,
parseDarwinSections,
parseElfSections,
}
Loading
Loading