Skip to content

Commit

Permalink
feat: add support for speedscope rendering of Android reactnative pro…
Browse files Browse the repository at this point in the history
…files (#386)

* Add support for speedscope rendering of reactnative profiles with mixed
android-reactNative profiles
  • Loading branch information
viglia authored Jan 12, 2024
1 parent 523342a commit 0bcf936
Show file tree
Hide file tree
Showing 3 changed files with 515 additions and 0 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

**Features**:

- Add support for speedscope rendering of Android reactnative profiles ([#386](https://github.com/getsentry/vroom/pull/386))

**Bug Fixes**:

**Internal**:
Expand Down
208 changes: 208 additions & 0 deletions internal/profile/legacy.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ import (
"time"

"github.com/getsentry/vroom/internal/debugmeta"
"github.com/getsentry/vroom/internal/frame"
"github.com/getsentry/vroom/internal/measurements"
"github.com/getsentry/vroom/internal/metadata"
"github.com/getsentry/vroom/internal/nodetree"
"github.com/getsentry/vroom/internal/platform"
"github.com/getsentry/vroom/internal/sample"
"github.com/getsentry/vroom/internal/speedscope"
"github.com/getsentry/vroom/internal/timeutil"
"github.com/getsentry/vroom/internal/transaction"
Expand All @@ -22,8 +24,11 @@ import (
const maxProfileDurationForCallTrees = 15 * time.Second

var ErrProfileHasNoTrace = errors.New("profile has no trace")
var member void

type (
void struct{}

LegacyProfile struct {
RawProfile

Expand Down Expand Up @@ -149,6 +154,29 @@ func (p LegacyProfile) IsSampleFormat() bool {
}

func (p *LegacyProfile) Speedscope() (speedscope.Output, error) {
t, ok := p.Trace.(Android)
// this is to handle only the Reactnative (android + js)
// use case. If it's an Android profile but there is no
// js profile, we'll skip this entirely
if ok && p.JsProfile != nil && len(p.JsProfile) > 0 {
st, err := unmarshalSampleProfile(p.JsProfile)
if err == nil {
// collect set of TIDs used and change main thread name
tidSet := make(map[uint64]void)
for i := range t.Threads {
tidSet[t.Threads[i].ID] = member
if t.Threads[i].Name == "main" {
t.Threads[i].Name = "android_main"
}
}

ap := sampleToAndroidFormat(st, uint64(len(t.Methods)), tidSet)
t.Events = append(t.Events, ap.Events...)
t.Methods = append(t.Methods, ap.Methods...)
t.Threads = append(t.Threads, ap.Threads...)
p.Trace = t
}
}
o, err := p.Trace.Speedscope()
if err != nil {
return speedscope.Output{}, err
Expand Down Expand Up @@ -264,3 +292,183 @@ func (p LegacyProfile) GetTransactionTags() map[string]string {
func (p LegacyProfile) GetMeasurements() map[string]measurements.Measurement {
return p.Measurements
}

// This is to be used for ReactNative JS profile only since it works based on the
// assumption that we'll only have 1 thread in the JS profile, as is the case
// for ReactNative.
// See: https://github.com/facebook/react-native-website/blob/43bc708c784be56b68a4d74711dd8824851b38f9/website/architecture/threading-model.md
func sampleToAndroidFormat(p sample.Trace, offset uint64, usedTids map[uint64]void) Android {
//var Clock Clock
var events []AndroidEvent
var methods []AndroidMethod
//var StartTime uint64
var threads []AndroidThread

var lastStack []int

methodSet := make(map[uint64]void)
threadSet := make(map[uint64]void)

newMainTID := getMainThreadIDs(p.ThreadMetadata, usedTids)

// we ignore (as we do for other platforms) the last sample
for si, sample := range p.Samples[:len(p.Samples)-1] {
eventTime := getEventTimeFromElapsedNanoseconds(sample.ElapsedSinceStartNS)
currentStack := p.Stacks[sample.StackID]
i := len(lastStack) - 1
j := len(currentStack) - 1
for i >= 0 && j >= 0 {
if lastStack[i] != currentStack[j] {
break
}
i--
j--
}
// at this point we've scanned through all the common frames at the bottom
// of the stack. For any frames left in the older stack we need to generate
// an "exit" event.
// This logic applies to all samples except the 1st
if si > 0 {
for z := 0; z <= i; z++ {
frameID := lastStack[z]
offsetID := uint64(frameID) + offset

ev := AndroidEvent{
Action: ExitAction,
ThreadID: newMainTID,
MethodID: offsetID,
Time: eventTime,
}

events = append(events, ev)
}
}

// For any frames left in the current stack we need to generate
// an "enter" event.
for ; j >= 0; j-- {
frameID := currentStack[j]
offsetID := uint64(frameID) + offset

if _, exists := methodSet[offsetID]; !exists {
updateMethods(methodSet, &methods, p.Frames[frameID], offsetID)
}
if _, exists := threadSet[newMainTID]; !exists {
metadata := p.ThreadMetadata[strconv.FormatUint(sample.ThreadID, 10)]
updateThreads(threadSet, &threads, newMainTID, &metadata)
}
ev := AndroidEvent{
Action: EnterAction,
ThreadID: newMainTID,
MethodID: offsetID,
Time: eventTime,
}
events = append(events, ev)
}
lastStack = currentStack
} // end sample loop

// we use the elapsed time since start of the latest sample
// to exit all the events of the previous one
closingTimeNs := p.Samples[len(p.Samples)-1].ElapsedSinceStartNS
eventTime := getEventTimeFromElapsedNanoseconds(closingTimeNs)

for i := 0; i < len(lastStack); i++ {
frameID := lastStack[i]
offsetID := uint64(frameID) + offset

ev := AndroidEvent{
Action: ExitAction,
ThreadID: newMainTID,
MethodID: offsetID,
Time: eventTime,
}

events = append(events, ev)
}

return Android{
Clock: DualClock,
Events: events,
Methods: methods,
Threads: threads,
}
}

func updateMethods(methodSet map[uint64]void, methods *[]AndroidMethod, fr frame.Frame, offsetID uint64) {
method := AndroidMethod{
ID: offsetID,
Name: fr.Function,
SourceFile: fr.Path,
SourceLine: fr.Line,
InApp: fr.InApp,
}
*methods = append(*methods, method)
methodSet[offsetID] = member
}

func updateThreads(threadSet map[uint64]void, threads *[]AndroidThread, threadID uint64, metadata *sample.ThreadMetadata) {
const mainThreadName = "JavaScriptThread"
thread := AndroidThread{
ID: threadID,
Name: metadata.Name,
}
// In a few other places (CallTree), we rely on the thread
// name "main" to figure out which thread data to use.
// For reactNative we want to ignore the Android "main"
// thread and instead use the js as the main one.
if thread.Name == mainThreadName {
thread.Name = "main"
}

*threads = append(*threads, thread)
threadSet[threadID] = member
}

// Native Android profile and JS profile have a thread ID in common
// As of now, we want to show them separately instead of merged
// To do so, the thread in the JS profile, will get a new ID
// that is not yet used by any of the threads in the native Android
// profile.
func getMainThreadIDs(threads map[string]sample.ThreadMetadata, usedTids map[uint64]void) uint64 {
const mainThreadName = "JavaScriptThread"
var newTid uint64
for id, threadMetadata := range threads {
if threadMetadata.Name == mainThreadName {
intNum, _ := strconv.ParseInt(id, 10, 64)
tid := uint64(intNum)
newTid = getUniqueTid(tid, usedTids)
break
}
}
return newTid
}

func getUniqueTid(tid uint64, usedTids map[uint64]void) uint64 {
for i := tid + 1; ; i++ {
if _, exists := usedTids[i]; !exists {
return i
}
}
}

func getEventTimeFromElapsedNanoseconds(ns uint64) EventTime {
return EventTime{
Monotonic: EventMonotonic{
Wall: Duration{
Secs: (ns / 1e9),
Nanos: (ns % 1e9),
},
},
}
}

func unmarshalSampleProfile(p json.RawMessage) (sample.Trace, error) {
var st sample.Trace
err := json.Unmarshal(p, &st)
if err != nil {
return sample.Trace{}, err
}

return st, nil
}
Loading

0 comments on commit 0bcf936

Please sign in to comment.