• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

jlfwong / speedscope / 8107195744

01 Mar 2024 05:52AM UTC coverage: 43.652% (+0.03%) from 43.619%
8107195744

Pull #466

github

web-flow
Merge 20bc6cebe into 0121cf934
Pull Request #466: Callgrind parser: Fixed associating file names with symbols and detection of root nodes

1181 of 2807 branches covered (42.07%)

Branch coverage included in aggregate %.

6 of 7 new or added lines in 1 file covered. (85.71%)

1 existing line in 1 file now uncovered.

2711 of 6109 relevant lines covered (44.38%)

4016.95 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

83.33
/src/import/callgrind.ts
1
// https://www.valgrind.org/docs/manual/cl-format.html
2
//
3
// Larger example files can be found by searching on github:
4
// https://github.com/search?q=cfn%3D&type=code
5
//
6
// Converting callgrind files into flamegraphs is challenging because callgrind
7
// formatted profiles contain call graphs with weighted nodes and edges, and
8
// such a weighted call graph does not uniquely define a flamegraph.
9
//
10
// Consider a program that looks like this:
11
//
12
//    // example.js
13
//    function backup(read) {
14
//      if (read) {
15
//        read()
16
//      } else {
17
//        write()
18
//      }
19
//    }
20
//
21
//    function start() {
22
//       backup(true)
23
//    }
24
//
25
//    function end() {
26
//       backup(false)
27
//    }
28
//
29
//    start()
30
//    end()
31
//
32
// Profiling this program might result in a profile that looks like the
33
// following flame graph defined in Brendan Gregg's plaintext format:
34
//
35
//    start;backup;read 4
36
//    end;backup;write 4
37
//
38
// When we convert this execution into a call-graph, we get the following:
39
//
40
//      +------------------+     +---------------+
41
//      | start (self: 0)  |     | end (self: 0) |
42
//      +------------------+     +---------------|
43
//                   \               /
44
//        (total: 4)  \             / (total: 4)
45
//                     v           v
46
//                 +------------------+
47
//                 | backup (self: 0) |
48
//                 +------------------+
49
//                    /            \
50
//       (total: 4)  /              \ (total: 4)
51
//                  v                v
52
//      +----------------+      +-----------------+
53
//      | read (self: 4) |      | write (self: 4) |
54
//      +----------------+      +-----------------+
55
//
56
// In the process of the conversion, we've lost information about the ratio of
57
// time spent in read v.s. write in the start call v.s. the end call. The
58
// following flame graph would yield the exact same call-graph, and therefore
59
// the exact sample call-grind formatted profile:
60
//
61
//    start;backup;read 3
62
//    start;backup;write 1
63
//    end;backup;read 1
64
//    end;backup;write 3
65
//
66
// This is unfortunate, since it means we can't produce a flamegraph that isn't
67
// potentially lying about the what the actual execution behavior was. To
68
// produce a flamegraph at all from the call graph representation, we have to
69
// decide how much weight each sub-call should have. Given that we know the
70
// total weight of each node, we'll make the incorrect assumption that every
71
// invocation of a function will have the average distribution of costs among
72
// the sub-function invocations. In the example given, this means we assume that
73
// every invocation of backup() is assumed to spend half its time in read() and
74
// half its time in write().
75
//
76
// So the flamegraph we'll produce from the given call-graph will actually be:
77
//
78
//    start;backup;read 2
79
//    start;backup;write 2
80
//    end;backup;read 2
81
//    end;backup;write 2
82
//
83
// A particularly bad consequence is that the resulting flamegraph will suggest
84
// that there was at some point a call stack that looked like
85
// strat;backup;write, even though that never happened in the real program
86
// execution.
87

88
import {CallTreeProfileBuilder, Frame, FrameInfo, Profile, ProfileGroup} from '../lib/profile'
34✔
89
import {getOrElse, getOrInsert, KeyedSet} from '../lib/utils'
34✔
90
import {ByteFormatter, TimeFormatter} from '../lib/value-formatters'
34✔
91
import {TextFileContent} from './utils'
92

93
class CallGraph {
94
  private frameSet = new KeyedSet<Frame>()
24✔
95
  private totalWeights = new Map<Frame, number>()
24✔
96
  private childrenTotalWeights = new Map<Frame, Map<Frame, number>>()
24✔
97

98
  constructor(
99
    private fileName: string,
24✔
100
    private fieldName: string,
24✔
101
  ) {}
102

103
  private getOrInsertFrame(info: FrameInfo): Frame {
104
    return Frame.getOrInsert(this.frameSet, info)
228✔
105
  }
106

107
  private addToTotalWeight(frame: Frame, weight: number) {
108
    if (!this.totalWeights.has(frame)) {
152✔
109
      this.totalWeights.set(frame, weight)
76✔
110
    } else {
111
      this.totalWeights.set(frame, this.totalWeights.get(frame)! + weight)
76✔
112
    }
113
  }
114

115
  addSelfWeight(frameInfo: FrameInfo, weight: number) {
116
    this.addToTotalWeight(this.getOrInsertFrame(frameInfo), weight)
76✔
117
  }
118

119
  addChildWithTotalWeight(parentInfo: FrameInfo, childInfo: FrameInfo, weight: number) {
120
    const parent = this.getOrInsertFrame(parentInfo)
76✔
121
    const child = this.getOrInsertFrame(childInfo)
76✔
122

123
    const childMap = getOrInsert(this.childrenTotalWeights, parent, k => new Map())
76✔
124

125
    if (!childMap.has(child)) {
76!
126
      childMap.set(child, weight)
76✔
127
    } else {
128
      childMap.set(child, childMap.get(child) + weight)
×
129
    }
130

131
    this.addToTotalWeight(parent, weight)
76✔
132
  }
133

134
  toProfile(): Profile {
135
    const profile = new CallTreeProfileBuilder()
24✔
136

137
    let unitMultiplier = 1
24✔
138

139
    // These are common field names used by Xdebug. Let's give them special
140
    // treatment to more helpfully display units.
141
    if (this.fieldName === 'Time_(10ns)') {
24✔
142
      profile.setName(`${this.fileName} -- Time`)
4✔
143
      unitMultiplier = 10
4✔
144
      profile.setValueFormatter(new TimeFormatter('nanoseconds'))
4✔
145
    } else if (this.fieldName == 'Memory_(bytes)') {
20✔
146
      profile.setName(`${this.fileName} -- Memory`)
4✔
147
      profile.setValueFormatter(new ByteFormatter())
4✔
148
    } else {
149
      profile.setName(`${this.fileName} -- ${this.fieldName}`)
16✔
150
    }
151

152
    let totalCumulative = 0
24✔
153

154
    const currentStack = new Set<Frame>()
24✔
155

156
    let maxWeight = 0
24✔
157
    for (let [_, totalWeight] of this.totalWeights) {
24✔
158
      maxWeight = Math.max(maxWeight, totalWeight)
76✔
159
    }
160

161
    const visit = (frame: Frame, subtreeTotalWeight: number) => {
24✔
162
      if (currentStack.has(frame)) {
100!
163
        // Call-graphs are allowed to have cycles. Call-trees are not. In case
164
        // we run into a cycle, we'll just avoid recursing into the same subtree
165
        // more than once in a call stack. The result will be that the time
166
        // spent in the recursive call will instead be attributed as self time
167
        // in the parent.
168
        return
×
169
      }
170

171
      // We need to calculate how much weight to give to a particular node in
172
      // the call-tree based on information from the call-graph. A given node
173
      // from the call-graph might correspond to several nodes in the call-tree,
174
      // so we need to decide how to distribute the weight of the call-graph
175
      // node to the various call-tree nodes.
176
      //
177
      // We assume that the weighting is evenly distributed. If a call-tree node
178
      // X occurs with weights x1 and x2, and we know from the call-graph that
179
      // child Y of X has a total weight y, then we assume the child Y of X has
180
      // weight y*x1/(x1 + x2) for the first occurrence, and y*x2(y1 + x2) for
181
      // the second occurrence.
182
      //
183
      // This assumption is incorrectly (sometimes wildly so), but we need to
184
      // make *some* assumption, and this seems to me the sanest option.
185
      //
186
      // See the comment at the top of the file for an example where this
187
      // assumption can yield especially misleading results.
188

189
      if (subtreeTotalWeight < 1e-4 * maxWeight) {
100!
190
        // This assumption about even distribution can cause us to generate a
191
        // call tree with dramatically more nodes than the call graph.
192
        //
193
        // Consider a function which is called 1000 times, where the result is
194
        // cached. The first invocation has a complex call tree and may take
195
        // 100ms. Let's say that this complex call tree has 250 nodes.
196
        //
197
        // Subsequent calls use the cached result, so take only 1ms, and have no
198
        // children in their call trees. So we have, in total, (1 + 250) + 999
199
        // nodes in the call-tree for a total of 1250 nodes.
200
        //
201
        // The information specific to each invocation is, however, lost in the
202
        // call-graph representation.
203
        //
204
        // Because of the even distribution assumption we make, this means that
205
        // the call-trees of each invocation will have the same shape. Each 1ms
206
        // call-tree will look identical to the 100ms call-tree, just
207
        // horizontally compacted. So instead of 1251 nodes, we have
208
        // 1000*250=250,000 nodes in the resulting call graph.
209
        //
210
        // To mitigate this explosion of the # of nodes, we ignore subtrees
211
        // whose weights are less than 0.01% of the heaviest node in the call
212
        // graph.
213
        return
×
214
      }
215

216
      const totalWeightForFrameInCallgraph = getOrElse(this.totalWeights, frame, () => 0)
100✔
217
      if (totalWeightForFrameInCallgraph === 0) {
100!
218
        return
×
219
      }
220

221
      let selfWeightForNodeInCallTree = subtreeTotalWeight
100✔
222

223
      profile.enterFrame(frame, Math.round(totalCumulative * unitMultiplier))
100✔
224

225
      currentStack.add(frame)
100✔
226
      for (let [child, totalWeightAsChild] of this.childrenTotalWeights.get(frame) || []) {
100✔
227
        // To determine the weight of the child in the call tree, we look at the
228
        // weight of the child in the call graph relative to its parent.
229
        const childCallTreeWeight =
230
          subtreeTotalWeight * (totalWeightAsChild / totalWeightForFrameInCallgraph)
76✔
231

232
        let prevTotalCumulative = totalCumulative
76✔
233
        visit(child, childCallTreeWeight)
76✔
234

235
        // Even though we tried to add a child with total weight equal to
236
        // childCallTreeWeight, we might have failed for a variety of data
237
        // consistency reasons, or due to cycles.
238
        //
239
        // We want to avoid losing weight in the call tree by subtracting from
240
        // the self weight on the assumption it was added to the subtree, so we
241
        // only subtree from the self weight the amount that was *actually* used
242
        // by the subtree, rather than the amount we *intended* for it to use.
243
        const actualChildCallTreeWeight = totalCumulative - prevTotalCumulative
76✔
244
        selfWeightForNodeInCallTree -= actualChildCallTreeWeight
76✔
245
      }
246
      currentStack.delete(frame)
100✔
247

248
      totalCumulative += selfWeightForNodeInCallTree
100✔
249
      profile.leaveFrame(frame, Math.round(totalCumulative * unitMultiplier))
100✔
250
    }
251

252
    // It's surprisingly hard to figure out which nodes in the call graph
253
    // constitute the root nodes of call trees.
254
    //
255
    // Here are a few intuitive options, and reasons why they're not always
256
    // correct or good.
257
    //
258
    // ## 1. Find nodes in the call graph that have no callers
259
    //
260
    // This is probably right 99% of the time in practice, but since the
261
    // callgrind is totally general, it's totally valid to have a file
262
    // representing a profile for the following code:
263
    //
264
    //    function a() {
265
    //      b()
266
    //    }
267
    //    function b() {
268
    //    }
269
    //    a()
270
    //    b()
271
    //
272
    // In this case, even though b has a caller, some of the real calltree for
273
    // an execution trace of the program will have b on the top of the stack.
274
    //
275
    // ## 2. Find nodes in the call graph that still have weight if you
276
    //       subtract all of the weight caused by callers.
277
    //
278
    // The callgraph format, in theory, provides inclusive times for every
279
    // function call. That means if you have a function `alpha` with a total
280
    // weight of 20, and its only in-edge in the call-graph has weight of 10,
281
    // that should indicate that `alpha` exists both as the root-node of a
282
    // calltree, and as a node in some other call-tree.
283
    //
284
    // In theory, you should be able to figure out the weight of it as a root
285
    // node by subtracting the weights of all the in-edges. In practice, real
286
    // callgrind files are inconsistent in how they do accounting for in-edges
287
    // where you end up in weird situations where the weight of in-edges
288
    // *exceeds* the weight of nodes (where the weight of a node is its
289
    // self-weight plus the weight of all its out-edges).
290
    //
291
    // ## 3. Find the heaviest node in the call graph, build its call-tree, and
292
    //       decrease the weights of other nodes in the call graph while you
293
    //       build the call tree. After you've done this, repeat with the new
294
    //       heaviest.
295
    //
296
    // I think this version is probably fully correct, but the performance is
297
    // awful. The naive-version is O(n^2) because you have to re-determine which
298
    // node is the heaviest after each time you finish building a call-tree. You
299
    // can't just sort, because the relative ordering also changes with the
300
    // construction of each call tree.
301
    //
302
    // There's probably a clever solution here which puts all of the nodes into
303
    // a min-heap and then deletes and re-inserts nodes as their weights change,
304
    // but reasoning about the performance of that is a big pain in the butt.
305
    //
306
    // Despite not always being correct, I'm opting for option (1).
307

308
    const rootNodes = new Set<Frame>(this.frameSet)
24✔
309

310
    for (let [_, childMap] of this.childrenTotalWeights) {
24✔
311
      for (let [child, _] of childMap) {
48✔
312
        rootNodes.delete(child)
76✔
313
      }
314
    }
315

316
    for (let rootNode of rootNodes) {
24✔
317
      visit(rootNode, this.totalWeights.get(rootNode)!)
24✔
318
    }
319

320
    return profile.build()
24✔
321
  }
322
}
323

324
// In writing this, I initially tried to use the formal grammar described in
325
// section 3.2 of https://www.valgrind.org/docs/manual/cl-format.html, but
326
// stopped because most of the information isn't relevant for visualization, and
327
// because there's inconsistency between the grammar and subsequence
328
// descriptions.
329
//
330
// For example, the grammar for headers specifies all the valid header names,
331
// but then the writing below that mentions there may be a "totals" or "summary"
332
// header, which should be disallowed by the formal grammar.
333
//
334
// So, instead, I'm not going to bother with a formal parse. Since there are no
335
// real recursive structures in this file format, that should be okay.
336
class CallgrindParser {
337
  private lines: string[]
338
  private lineNum: number
339

340
  private callGraphs: CallGraph[] | null = null
20✔
341
  private eventsLine: string | null = null
20✔
342

343
  private filename: string | null = null
20✔
344
  private functionName: string | null = null
20✔
345
  private functionFile: string | null = null
20✔
346
  private currentFunctionFile: string | null = null
20✔
347
  private calleeFilename: string | null = null
20✔
348
  private calleeFunctionName: string | null = null
20✔
349

350
  private savedFileNames: {[id: string]: string} = {}
20✔
351
  private savedFunctionNames: {[id: string]: string} = {}
20✔
352

353
  constructor(
354
    contents: TextFileContent,
355
    private importedFileName: string,
20✔
356
  ) {
357
    this.lines = [...contents.splitLines()]
20✔
358
    this.lineNum = 0
20✔
359
  }
360

361
  parse(): ProfileGroup | null {
362
    while (this.lineNum < this.lines.length) {
20✔
363
      const line = this.lines[this.lineNum++]
460✔
364

365
      if (/^\s*#/.exec(line)) {
460✔
366
        // Line is a comment. Ignore it.
367
        continue
16✔
368
      }
369

370
      if (/^\s*$/.exec(line)) {
444✔
371
        // Line is empty. Ignore it.
372
        continue
68✔
373
      }
374

375
      if (this.parseHeaderLine(line)) {
376✔
376
        continue
40✔
377
      }
378

379
      if (this.parseAssignmentLine(line)) {
336✔
380
        continue
272✔
381
      }
382

383
      if (this.parseCostLine(line, 'self')) {
64!
384
        continue
64✔
385
      }
386

387
      throw new Error(`Unrecognized line "${line}" on line ${this.lineNum}`)
×
388
    }
389

390
    if (!this.callGraphs) {
20!
391
      return null
×
392
    }
393
    return {
20✔
394
      name: this.importedFileName,
395
      indexToView: 0,
396
      profiles: this.callGraphs.map(cg => cg.toProfile()),
24✔
397
    }
398
  }
399

400
  private frameInfo(): FrameInfo {
401
    const file = this.functionFile || '(unknown)'
152!
402
    const name = this.functionName || '(unknown)'
152!
403
    const key = `${file}:${name}`
152✔
404
    return {key, name, file}
152✔
405
  }
406

407
  private calleeFrameInfo(): FrameInfo {
408
    const file = this.calleeFilename || this.filename || '(unknown)'
76!
409
    const name = this.calleeFunctionName || '(unknown)'
76!
410
    const key = `${file}:${name}`
76✔
411
    return {key, name, file}
76✔
412
  }
413

414
  private parseHeaderLine(line: string): boolean {
415
    const headerMatch = /^\s*(\w+):\s*(.*)+$/.exec(line)
376✔
416
    if (!headerMatch) return false
376✔
417

418
    if (headerMatch[1] !== 'events') {
40✔
419
      // We don't care about other headers. Ignore this line.
420
      return true
20✔
421
    }
422

423
    // Line specifies the formatting of subsequent cost lines.
424
    const fields = headerMatch[2].split(' ')
20✔
425

426
    if (this.callGraphs != null) {
20!
427
      throw new Error(
×
428
        `Duplicate "events: " lines specified. First was "${this.eventsLine}", now received "${line}" on ${this.lineNum}.`,
429
      )
430
    }
431

432
    this.callGraphs = fields.map(fieldName => {
20✔
433
      return new CallGraph(this.importedFileName, fieldName)
24✔
434
    })
435

436
    return true
20✔
437
  }
438

439
  private parseAssignmentLine(line: string): boolean {
440
    const assignmentMatch = /^(\w+)=\s*(.*)$/.exec(line)
336✔
441
    if (!assignmentMatch) return false
336✔
442

443
    const key = assignmentMatch[1]
272✔
444
    const value = assignmentMatch[2]
272✔
445

446
    switch (key) {
272✔
447
      case 'fe':
304!
448
      case 'fi': {
449
        // fe/fi are used to indicate the source-file of a function definition
450
        // changed mid-definition. This is for inlined code, but when a function
451
        // is called from within the inlined code, it is indicated that it
452
        // comes from file marked by fe/fi.
NEW
453
        this.filename = this.parseNameWithCompression(value, this.savedFileNames)
×
UNCOV
454
        break
×
455
      }
456

457
      case 'fl': {
458
        this.filename = this.parseNameWithCompression(value, this.savedFileNames)
40✔
459
        // The 'fl' needs to be stored in order to reset current filename upon
460
        // consecutive 'fn' calls
461
        this.currentFunctionFile = this.filename
40✔
462
        break
40✔
463
      }
464

465
      case 'fn': {
466
        // the file for a function defined with 'fn' always comes from 'fl'
467
        // It also resets the current filename to previous 'fl' value, as in KCacheGrind:
468
        // https://github.com/KDE/kcachegrind/blob/ea4314db2785cb8f279fe884ee7f82445642b692/libcore/cachegrindloader.cpp#L808
469
        if (this.filename !== this.currentFunctionFile) this.filename = this.currentFunctionFile
64!
470
        this.functionName = this.parseNameWithCompression(value, this.savedFunctionNames)
64✔
471
        // store function file associated with 'fn' function
472
        this.functionFile = this.filename
64✔
473
        break
64✔
474
      }
475

476
      case 'cfi':
477
      case 'cfl': {
478
        // NOTE: unlike the fe/fi distinction described above, cfi and cfl are
479
        // interchangeable.
480
        this.calleeFilename = this.parseNameWithCompression(value, this.savedFileNames)
40✔
481
        break
40✔
482
      }
483

484
      case 'cfn': {
485
        this.calleeFunctionName = this.parseNameWithCompression(value, this.savedFunctionNames)
64✔
486
        break
64✔
487
      }
488

489
      case 'calls': {
490
        // TODO(jlfwong): This is currently ignoring the number of calls being
491
        // made. Accounting for the number of calls might be unhelpful anyway,
492
        // since it'll just be copying the exact same frame over-and-over again,
493
        // but that might be better than ignoring it.
494
        this.parseCostLine(this.lines[this.lineNum++], 'child')
64✔
495

496
        // This isn't specified anywhere in the spec, but empirically the and
497
        // "cfn" scope should only persist for a single "call".
498
        //
499
        // This seems to be what KCacheGrind does too:
500
        //
501
        // https://github.com/KDE/kcachegrind/blob/ea4314db2785cb8f279fe884ee7f82445642b692/libcore/cachegrindloader.cpp#L1259
502
        this.calleeFilename = null
64✔
503
        this.calleeFunctionName = null
64✔
504
        break
64✔
505
      }
506

507
      case 'cob':
508
      case 'ob': {
509
        // We ignore these for now. They're valid lines, but we don't capture or
510
        // display information about them.
511
        break
×
512
      }
513

514
      default: {
515
        console.log(`Ignoring assignment to unrecognized key "${line}" on line ${this.lineNum}`)
×
516
      }
517
    }
518

519
    return true
272✔
520
  }
521

522
  private parseNameWithCompression(name: string, saved: {[id: string]: string}): string {
523
    {
524
      const nameDefinitionMatch = /^\((\d+)\)\s*(.+)$/.exec(name)
208✔
525

526
      if (nameDefinitionMatch) {
208✔
527
        const id = nameDefinitionMatch[1]
40✔
528
        const name = nameDefinitionMatch[2]
40✔
529
        if (id in saved) {
40!
530
          throw new Error(
×
531
            `Redefinition of name with id: ${id}. Original value was "${saved[id]}". Tried to redefine as "${name}" on line ${this.lineNum}.`,
532
          )
533
        }
534

535
        saved[id] = name
40✔
536
        return name
40✔
537
      }
538
    }
539

540
    {
541
      const nameUseMatch = /^\((\d+)\)$/.exec(name)
168✔
542
      if (nameUseMatch) {
168✔
543
        const id = nameUseMatch[1]
40✔
544
        if (!(id in saved)) {
40!
545
          throw new Error(
×
546
            `Tried to use name with id ${id} on line ${this.lineNum} before it was defined.`,
547
          )
548
        }
549
        return saved[id]
40✔
550
      }
551
    }
552

553
    return name
128✔
554
  }
555

556
  private prevCostLineNumbers: number[] = []
20✔
557

558
  private parseCostLine(line: string, costType: 'self' | 'child'): boolean {
559
    // TODO(jlfwong): Allow hexadecimal encoding
560

561
    const parts = line.split(/\s+/)
128✔
562
    const nums: number[] = []
128✔
563

564
    for (let i = 0; i < parts.length; i++) {
128✔
565
      const part = parts[i]
280✔
566

567
      if (part.length === 0) {
280!
568
        return false
×
569
      }
570

571
      if (part === '*' || part[0] === '-' || part[1] === '+') {
280✔
572
        // This handles "Subposition compression"
573
        // See: https://valgrind.org/docs/manual/cl-format.html#cl-format.overview.compression2
574
        if (this.prevCostLineNumbers.length <= i) {
24!
575
          throw new Error(
×
576
            `Line ${this.lineNum} has a subposition on column ${i} but ` +
577
              `previous cost line has only ${this.prevCostLineNumbers.length} ` +
578
              `columns. Line contents: ${line}`,
579
          )
580
        }
581
        const prevCostForSubposition = this.prevCostLineNumbers[i]
24✔
582
        if (part === '*') {
24✔
583
          nums.push(prevCostForSubposition)
16✔
584
        } else {
585
          // This handles both the '-' and '+' cases
586
          const offset = parseInt(part)
8✔
587
          if (isNaN(offset)) {
8!
588
            throw new Error(
×
589
              `Line ${this.lineNum} has a subposition on column ${i} but ` +
590
                `the offset is not a number. Line contents: ${line}`,
591
            )
592
          }
593
          nums.push(prevCostForSubposition + offset)
8✔
594
        }
595
      } else {
596
        const asNum = parseInt(part)
256✔
597
        if (isNaN(asNum)) {
256!
598
          return false
×
599
        }
600
        nums.push(asNum)
256✔
601
      }
602
    }
603

604
    if (nums.length == 0) {
128!
605
      return false
×
606
    }
607

608
    // TODO(jlfwong): Handle custom positions format w/ multiple parts
609
    const numPositionFields = 1
128✔
610

611
    // NOTE: We intentionally do not include the line number here because
612
    // callgrind uses the line number of the function invocation, not the
613
    // line number of the function definition, which conflicts with how
614
    // speedscope uses line numbers.
615
    //
616
    // const lineNum = nums[0]
617

618
    if (!this.callGraphs) {
128!
619
      throw new Error(
×
620
        `Encountered a cost line on line ${this.lineNum} before event specification was provided.`,
621
      )
622
    }
623
    for (let i = 0; i < this.callGraphs.length; i++) {
128✔
624
      if (costType === 'self') {
152✔
625
        this.callGraphs[i].addSelfWeight(this.frameInfo(), nums[numPositionFields + i])
76✔
626
      } else if (costType === 'child') {
76!
627
        this.callGraphs[i].addChildWithTotalWeight(
76✔
628
          this.frameInfo(),
629
          this.calleeFrameInfo(),
630
          nums[numPositionFields + i] || 0,
76!
631
        )
632
      }
633
    }
634

635
    this.prevCostLineNumbers = nums
128✔
636
    return true
128✔
637
  }
638
}
639

640
export function importFromCallgrind(
34✔
641
  contents: TextFileContent,
642
  importedFileName: string,
643
): ProfileGroup | null {
644
  return new CallgrindParser(contents, importedFileName).parse()
20✔
645
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc