Skip to content

Commit

Permalink
feat: add utility to merge a list of android chunks and generate a sp…
Browse files Browse the repository at this point in the history
…eedscope result (#531)

* add utility to merge a list of android chunks and generate a speedscope
result
  • Loading branch information
viglia authored Nov 29, 2024
1 parent 638be86 commit 6eb8e55
Show file tree
Hide file tree
Showing 6 changed files with 701 additions and 14 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@
- Update materialized_version for profile functions metrics ([#522](https://github.com/getsentry/vroom/pull/522))
- Support writing functions metrics we extract from chunks into the functions dataset ([#524](https://github.com/getsentry/vroom/pull/524))
- Keep top N samples in flamegraph. ([#526](https://github.com/getsentry/vroom/pull/526))
- Add utility to merge a list of android chunks and generate a speedscope result ([#531](https://github.com/getsentry/vroom/pull/531))
- Remove unused legacy flamegraph code path. ([#533](https://github.com/getsentry/vroom/pull/533))
- Remove generic metrics ingestion ([#534](https://github.com/getsentry/vroom/pull/534))

Expand Down
181 changes: 181 additions & 0 deletions internal/chunk/android_utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
package chunk

import (
"encoding/json"
"sort"
"time"

"github.com/getsentry/vroom/internal/measurements"
"github.com/getsentry/vroom/internal/profile"
"github.com/getsentry/vroom/internal/speedscope"
)

var member void

type void struct{}

func SpeedscopeFromAndroidChunks(chunks []AndroidChunk, startTS, endTS uint64) (speedscope.Output, error) {
if len(chunks) == 0 {
return speedscope.Output{}, nil
}
maxTsNS := uint64(0)
threadSet := make(map[uint64]void)
// fingerprint to method ID
methodToID := make(map[uint32]uint64)
sort.Slice(chunks, func(i, j int) bool {
return chunks[i].EndTimestamp() <= chunks[j].StartTimestamp()
})

mergedMeasurement := make(map[string]measurements.MeasurementV2)

chunk := chunks[0]
firstChunkStartTimestampNS := uint64(chunk.StartTimestamp() * 1e9)
// Initially, adjustedChunkStartTimestampNS will just be the
// chunk timestamp. If the chunk starts before the allowed
// time range though, we only keep events that fall within
// the range, set the startTimestamp to the start value of
// the allowed range and adjust the relative ts of each
// events.
adjustedChunkStartTimestampNS := firstChunkStartTimestampNS
buildTimestamp := chunk.Profile.TimestampGetter()
// clean up the events in the first chunk
events := make([]profile.AndroidEvent, 0, len(chunk.Profile.Events))
methods := make([]profile.AndroidMethod, 0, len(chunk.Profile.Methods))
// updates methods ID
tmpMethodsID := make(map[uint64]uint64)
for i, method := range chunk.Profile.Methods {
id := uint64(i + 1)
tmpMethodsID[method.ID] = id
method.ID = id
methodToID[method.Frame().Fingerprint()] = id
methods = append(methods, method)
}
delta := int64(0)
if firstChunkStartTimestampNS < startTS {
delta = -int64(startTS - firstChunkStartTimestampNS)
adjustedChunkStartTimestampNS = startTS
}
addTimeDelta := chunk.Profile.AddTimeDelta(delta)
for _, event := range chunk.Profile.Events {
ts := buildTimestamp(event.Time) + firstChunkStartTimestampNS
if ts < startTS || ts > endTS {
// we filter out events out of range
continue
}
// If the event falls within allowed range, but the first chunk
// begins before the start range (delta != 0), adjust the relative ts
// of each event by subtracting the delta.
if delta != 0 {
err := addTimeDelta(&event)
if err != nil {
return speedscope.Output{}, err
}
// update ts
ts = buildTimestamp(event.Time) + adjustedChunkStartTimestampNS
}
event.MethodID = tmpMethodsID[event.MethodID]
events = append(events, event)
maxTsNS = max(maxTsNS, ts)
}
for _, thread := range chunk.Profile.Threads {
threadSet[thread.ID] = member
}
if len(chunk.Measurements) > 0 {
err := json.Unmarshal(chunk.Measurements, &mergedMeasurement)
if err != nil {
return speedscope.Output{}, err
}
}

// If chunk started before the allowed time range
// update the chunk timestamp (firstChunkStartTimestampNS)
// since later on, other chunks will use this to compute
// the right offset (relative ts in nanoseconds).
if delta != 0 {
firstChunkStartTimestampNS = adjustedChunkStartTimestampNS
}

for i := 1; i < len(chunks); i++ {
c := chunks[i]
chunkStartTimestampNs := uint64(c.StartTimestamp() * 1e9)
buildTimestamp := c.Profile.TimestampGetter()
// Delta between the current chunk timestamp and the very first one.
// This will be needed to correctly offset the events relative ts,
// which need to be relative not to the start of this chunk, but to
// the start of the very first one.
delta := chunkStartTimestampNs - firstChunkStartTimestampNS
addTimeDelta := c.Profile.AddTimeDelta(int64(delta))
// updates methods ID
tmpMethodsID = make(map[uint64]uint64)
for _, method := range c.Profile.Methods {
fingerprint := method.Frame().Fingerprint()
if id, ok := methodToID[fingerprint]; !ok {
newID := uint64(len(methodToID) + 1)
methodToID[fingerprint] = newID
tmpMethodsID[method.ID] = newID
method.ID = newID
methods = append(methods, method)
} else {
tmpMethodsID[method.ID] = id
}
}

// filter events
for _, event := range c.Profile.Events {
ts := buildTimestamp(event.Time) + chunkStartTimestampNs
if ts < startTS || ts > endTS {
continue
}
event.MethodID = tmpMethodsID[event.MethodID]
// Before adding the event, update its relative timestamp
// which, in this case, should not be relative to the current
// chunk timestamp, but rather relative to the very 1st one.
err := addTimeDelta(&event)
if err != nil {
return speedscope.Output{}, err
}
ts = buildTimestamp(event.Time) + firstChunkStartTimestampNS
events = append(events, event)
maxTsNS = max(maxTsNS, ts)
}
// Update threads.
for _, thread := range c.Profile.Threads {
if _, ok := threadSet[thread.ID]; !ok {
chunk.Profile.Threads = append(c.Profile.Threads, thread)
threadSet[thread.ID] = member
}
}
// In case we have measurements, merge them too.
if len(c.Measurements) > 0 {
var chunkMeasurements map[string]measurements.MeasurementV2
err := json.Unmarshal(c.Measurements, &chunkMeasurements)
if err != nil {
return speedscope.Output{}, err
}
for k, measurement := range chunkMeasurements {
if el, ok := mergedMeasurement[k]; ok {
el.Values = append(el.Values, measurement.Values...)
mergedMeasurement[k] = el
} else {
mergedMeasurement[k] = measurement
}
}
}
}
chunk.Profile.Events = events
chunk.Profile.Methods = methods
chunk.DurationNS = maxTsNS

s, err := chunk.Profile.Speedscope()
if err != nil {
return speedscope.Output{}, err
}
s.DurationNS = chunk.DurationNS
s.Metadata.Timestamp = time.Unix(0, int64(firstChunkStartTimestampNS)).UTC()

if len(mergedMeasurement) > 0 {
s.MeasurementsV2 = mergedMeasurement
}

return s, nil
}
Loading

0 comments on commit 6eb8e55

Please sign in to comment.