Skip to content

Commit 4e2e716

Browse files
authored
Add filesystem events to the timeline (#4965)
1 parent e36f26b commit 4e2e716

File tree

6 files changed

+263
-39
lines changed

6 files changed

+263
-39
lines changed
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
const fs = require('fs')
2+
const os = require('os')
3+
const path = require('path')
4+
5+
const tracer = require('dd-trace').init()
6+
tracer.profilerStarted().then(() => {
7+
tracer.trace('x', (_, done) => {
8+
setImmediate(() => {
9+
// Generate 1MB of random data
10+
const buffer = Buffer.alloc(1024 * 1024)
11+
for (let i = 0; i < buffer.length; i++) {
12+
buffer[i] = Math.floor(Math.random() * 256)
13+
}
14+
15+
// Create a temporary file
16+
const tempFilePath = path.join(os.tmpdir(), 'tempfile.txt')
17+
18+
fs.writeFile(tempFilePath, buffer, (err) => {
19+
if (err) throw err
20+
21+
// Read the data back
22+
setImmediate(() => {
23+
fs.readFile(tempFilePath, (err, readData) => {
24+
setImmediate(() => {
25+
// Delete the temporary file
26+
fs.unlink(tempFilePath, (err) => {
27+
if (err) throw err
28+
})
29+
done()
30+
})
31+
if (err) throw err
32+
if (Buffer.compare(buffer, readData) !== 0) {
33+
throw new Error('Data read from file is different from data written to file')
34+
}
35+
})
36+
})
37+
})
38+
})
39+
})
40+
})

integration-tests/profiler/profiler.spec.js

Lines changed: 141 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,108 @@ function expectTimeout (messagePromise, allowErrors = false) {
104104
)
105105
}
106106

107+
class TimelineEventProcessor {
108+
constructor (strings, encoded) {
109+
this.strings = strings
110+
this.encoded = encoded
111+
}
112+
}
113+
114+
class NetworkEventProcessor extends TimelineEventProcessor {
115+
constructor (strings, encoded) {
116+
super(strings, encoded)
117+
118+
this.hostKey = strings.dedup('host')
119+
this.addressKey = strings.dedup('address')
120+
this.portKey = strings.dedup('port')
121+
}
122+
123+
processLabel (label, processedLabels) {
124+
switch (label.key) {
125+
case this.hostKey:
126+
processedLabels.host = label.str
127+
return true
128+
case this.addressKey:
129+
processedLabels.address = label.str
130+
return true
131+
case this.portKey:
132+
processedLabels.port = label.num
133+
return true
134+
default:
135+
return false
136+
}
137+
}
138+
139+
decorateEvent (ev, pl) {
140+
// Exactly one of these is defined
141+
assert.isTrue(!!pl.address !== !!pl.host, this.encoded)
142+
if (pl.address) {
143+
ev.address = this.strings.strings[pl.address]
144+
} else {
145+
ev.host = this.strings.strings[pl.host]
146+
}
147+
if (pl.port) {
148+
ev.port = pl.port
149+
}
150+
}
151+
}
152+
107153
async function gatherNetworkTimelineEvents (cwd, scriptFilePath, eventType, args) {
154+
return gatherTimelineEvents(cwd, scriptFilePath, eventType, args, NetworkEventProcessor)
155+
}
156+
157+
class FilesystemEventProcessor extends TimelineEventProcessor {
158+
constructor (strings, encoded) {
159+
super(strings, encoded)
160+
161+
this.fdKey = strings.dedup('fd')
162+
this.fileKey = strings.dedup('file')
163+
this.flagKey = strings.dedup('flag')
164+
this.modeKey = strings.dedup('mode')
165+
this.pathKey = strings.dedup('path')
166+
}
167+
168+
processLabel (label, processedLabels) {
169+
switch (label.key) {
170+
case this.fdKey:
171+
processedLabels.fd = label.num
172+
return true
173+
case this.fileKey:
174+
processedLabels.file = label.str
175+
return true
176+
case this.flagKey:
177+
processedLabels.flag = label.str
178+
return true
179+
case this.modeKey:
180+
processedLabels.mode = label.str
181+
return true
182+
case this.pathKey:
183+
processedLabels.path = label.str
184+
return true
185+
default:
186+
return false
187+
}
188+
}
189+
190+
decorateEvent (ev, pl) {
191+
ev.fd = pl.fd
192+
ev.file = this.strings.strings[pl.file]
193+
ev.flag = this.strings.strings[pl.flag]
194+
ev.mode = this.strings.strings[pl.mode]
195+
ev.path = this.strings.strings[pl.path]
196+
for (const [k, v] of Object.entries(ev)) {
197+
if (v === undefined) {
198+
delete ev[k]
199+
}
200+
}
201+
}
202+
}
203+
204+
async function gatherFilesystemTimelineEvents (cwd, scriptFilePath) {
205+
return gatherTimelineEvents(cwd, scriptFilePath, 'fs', [], FilesystemEventProcessor)
206+
}
207+
208+
async function gatherTimelineEvents (cwd, scriptFilePath, eventType, args, Processor) {
108209
const procStart = BigInt(Date.now() * 1000000)
109210
const proc = fork(path.join(cwd, scriptFilePath), args, {
110211
cwd,
@@ -123,60 +224,50 @@ async function gatherNetworkTimelineEvents (cwd, scriptFilePath, eventType, args
123224
const strings = profile.stringTable
124225
const tsKey = strings.dedup('end_timestamp_ns')
125226
const eventKey = strings.dedup('event')
126-
const hostKey = strings.dedup('host')
127-
const addressKey = strings.dedup('address')
128-
const portKey = strings.dedup('port')
129-
const nameKey = strings.dedup('operation')
227+
const operationKey = strings.dedup('operation')
130228
const spanIdKey = strings.dedup('span id')
131229
const localRootSpanIdKey = strings.dedup('local root span id')
132230
const eventValue = strings.dedup(eventType)
133231
const events = []
232+
const processor = new Processor(strings, encoded)
134233
for (const sample of profile.sample) {
135-
let ts, event, host, address, port, name, spanId, localRootSpanId
234+
let ts, event, operation, spanId, localRootSpanId
235+
const processedLabels = {}
136236
const unexpectedLabels = []
137237
for (const label of sample.label) {
138238
switch (label.key) {
139239
case tsKey: ts = label.num; break
140-
case nameKey: name = label.str; break
240+
case operationKey: operation = label.str; break
141241
case eventKey: event = label.str; break
142-
case hostKey: host = label.str; break
143-
case addressKey: address = label.str; break
144-
case portKey: port = label.num; break
145242
case spanIdKey: spanId = label.str; break
146243
case localRootSpanIdKey: localRootSpanId = label.str; break
147-
default: unexpectedLabels.push(label.key)
244+
default:
245+
if (!processor.processLabel(label, processedLabels)) {
246+
unexpectedLabels.push(label.key)
247+
}
148248
}
149249
}
150-
// Gather only DNS events; ignore sporadic GC events
250+
// Timestamp must be defined and be between process start and end time
251+
assert.isDefined(ts, encoded)
252+
assert.isTrue(ts <= procEnd, encoded)
253+
assert.isTrue(ts >= procStart, encoded)
254+
// Gather only tested events
151255
if (event === eventValue) {
152-
// Timestamp must be defined and be between process start and end time
153-
assert.isDefined(ts, encoded)
154-
assert.isTrue(ts <= procEnd, encoded)
155-
assert.isTrue(ts >= procStart, encoded)
156256
if (process.platform !== 'win32') {
157257
assert.isDefined(spanId, encoded)
158258
assert.isDefined(localRootSpanId, encoded)
159259
} else {
160260
assert.isUndefined(spanId, encoded)
161261
assert.isUndefined(localRootSpanId, encoded)
162262
}
163-
assert.isDefined(name, encoded)
263+
assert.isDefined(operation, encoded)
164264
if (unexpectedLabels.length > 0) {
165265
const labelsStr = JSON.stringify(unexpectedLabels)
166266
const labelsStrStr = unexpectedLabels.map(k => strings.strings[k]).join(',')
167267
assert.fail(`Unexpected labels: ${labelsStr}\n${labelsStrStr}\n${encoded}`)
168268
}
169-
// Exactly one of these is defined
170-
assert.isTrue(!!address !== !!host, encoded)
171-
const ev = { name: strings.strings[name] }
172-
if (address) {
173-
ev.address = strings.strings[address]
174-
} else {
175-
ev.host = strings.strings[host]
176-
}
177-
if (port) {
178-
ev.port = port
179-
}
269+
const ev = { operation: strings.strings[operation] }
270+
processor.decorateEvent(ev, processedLabels)
180271
events.push(ev)
181272
}
182273
}
@@ -323,14 +414,30 @@ describe('profiler', () => {
323414
assert.equal(endpoints.size, 3, encoded)
324415
})
325416

417+
it('fs timeline events work', async () => {
418+
const fsEvents = await gatherFilesystemTimelineEvents(cwd, 'profiler/fstest.js')
419+
assert.equal(fsEvents.length, 6)
420+
const path = fsEvents[0].path
421+
const fd = fsEvents[1].fd
422+
assert(path.endsWith('tempfile.txt'))
423+
assert.sameDeepMembers(fsEvents, [
424+
{ flag: 'w', mode: '', operation: 'open', path },
425+
{ fd, operation: 'write' },
426+
{ fd, operation: 'close' },
427+
{ file: path, operation: 'writeFile' },
428+
{ operation: 'readFile', path },
429+
{ operation: 'unlink', path }
430+
])
431+
})
432+
326433
it('dns timeline events work', async () => {
327434
const dnsEvents = await gatherNetworkTimelineEvents(cwd, 'profiler/dnstest.js', 'dns')
328435
assert.sameDeepMembers(dnsEvents, [
329-
{ name: 'lookup', host: 'example.org' },
330-
{ name: 'lookup', host: 'example.com' },
331-
{ name: 'lookup', host: 'datadoghq.com' },
332-
{ name: 'queryA', host: 'datadoghq.com' },
333-
{ name: 'lookupService', address: '13.224.103.60', port: 80 }
436+
{ operation: 'lookup', host: 'example.org' },
437+
{ operation: 'lookup', host: 'example.com' },
438+
{ operation: 'lookup', host: 'datadoghq.com' },
439+
{ operation: 'queryA', host: 'datadoghq.com' },
440+
{ operation: 'lookupService', address: '13.224.103.60', port: 80 }
334441
])
335442
})
336443

@@ -366,8 +473,8 @@ describe('profiler', () => {
366473
// The profiled program should have two TCP connection events to the two
367474
// servers.
368475
assert.sameDeepMembers(events, [
369-
{ name: 'connect', host: '127.0.0.1', port: port1 },
370-
{ name: 'connect', host: '127.0.0.1', port: port2 }
476+
{ operation: 'connect', host: '127.0.0.1', port: port1 },
477+
{ operation: 'connect', host: '127.0.0.1', port: port2 }
371478
])
372479
} finally {
373480
server2.close()

packages/datadog-instrumentations/src/fs.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ const errorChannel = channel('apm:fs:operation:error')
1313
const ddFhSym = Symbol('ddFileHandle')
1414
let kHandle, kDirReadPromisified, kDirClosePromisified
1515

16+
// Update packages/dd-trace/src/profiling/profilers/event_plugins/fs.js if you make changes to param names in any of
17+
// the following objects.
18+
1619
const paramsByMethod = {
1720
access: ['path', 'mode'],
1821
appendFile: ['path', 'data', 'options'],

packages/dd-trace/src/profiling/profilers/event_plugins/event.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,11 @@ class EventPlugin extends TracingPlugin {
3232
if (!store) return
3333

3434
const { startEvent, startTime, error } = store
35-
if (error) {
36-
return // don't emit perf events for failed operations
35+
if (error || this.ignoreEvent(startEvent)) {
36+
return // don't emit perf events for failed operations or ignored events
3737
}
38-
const duration = performance.now() - startTime
3938

39+
const duration = performance.now() - startTime
4040
const event = {
4141
entryType: this.entryType,
4242
startTime,
@@ -53,6 +53,10 @@ class EventPlugin extends TracingPlugin {
5353

5454
this.eventHandler(this.extendEvent(event, startEvent))
5555
}
56+
57+
ignoreEvent () {
58+
return false
59+
}
5660
}
5761

5862
module.exports = EventPlugin
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
const EventPlugin = require('./event')
2+
3+
// Values taken from parameter names in datadog-instrumentations/src/fs.js.
4+
// Known param names that are disallowed because they can be strings and have arbitrary sizes:
5+
// 'data'
6+
// Known param names that are disallowed because they are never a string or number:
7+
// 'buffer', 'buffers', 'listener'
8+
const allowedParams = new Set([
9+
'atime', 'dest',
10+
'existingPath', 'fd', 'file',
11+
'flag', 'gid', 'len',
12+
'length', 'mode', 'mtime',
13+
'newPath', 'offset', 'oldPath',
14+
'operation', 'options', 'path',
15+
'position', 'prefix', 'src',
16+
'target', 'type', 'uid'
17+
])
18+
19+
class FilesystemPlugin extends EventPlugin {
20+
static get id () {
21+
return 'fs'
22+
}
23+
24+
static get operation () {
25+
return 'operation'
26+
}
27+
28+
static get entryType () {
29+
return 'fs'
30+
}
31+
32+
ignoreEvent (event) {
33+
// Don't care about sync events, they show up in the event loop samples anyway
34+
return event.operation?.endsWith('Sync')
35+
}
36+
37+
extendEvent (event, detail) {
38+
const d = { ...detail }
39+
Object.entries(d).forEach(([k, v]) => {
40+
if (!(allowedParams.has(k) && (typeof v === 'string' || typeof v === 'number'))) {
41+
delete d[k]
42+
}
43+
})
44+
event.detail = d
45+
46+
return event
47+
}
48+
}
49+
module.exports = FilesystemPlugin

0 commit comments

Comments
 (0)