Sample Format V1
Transaction based profiling.
For the Sample Format V1, a profile should always be associated with a transaction and it should be sent in the same envelope as the associated transaction.
{
"debug_meta": {
"images": [
{
"debug_id": "32420279-25E2-34E6-8BC7-8A006A8F2425",
"image_addr": "0x000000010258c000",
"code_file": "/private/var/containers/Bundle/Application/C3511752-DD67-4FE8-9DA2-ACE18ADFAA61/TrendingMovies.app/TrendingMovies",
"type": "macho",
"image_size": 1720320,
"image_vmaddr": "0x0000000100000000"
}
]
},
"device": {
"architecture": "arm64e",
"is_emulator": true,
"locale": "en_US",
"manufacturer": "Apple",
"model": "iPhone14,8"
},
"environment": "development",
"event_id": "41fed0925670468bb0457f61a74688ec",
"measurements": { ... },
"os": {
"build_number": "20D47",
"name": "iOS",
"version": "16.3",
},
"platform": "cocoa",
"release": "1.0 (9999)",
"runtime": {
"name": "",
"version": ""
},
"timestamp": "2023-01-01T00:00:00.000Z",
"transaction": {
"active_thread_id": "259",
"id": "30976f2ddbe04ac9b6bffe6e35d4710c",
"name": "example_ios_movies_sources.MoviesViewController",
"trace_id": "4b25bc58f14243d8b208d1e22a054164",
},
"version": "1",
"profile": {
"samples": [
{
"elapsed_since_start_ns": 1234567890,
"stack_id": 0,
"thread_id": "259"
}
],
"stacks": [
[ 0 ]
],
"frames": [
{
"instruction_addr": "0xa722447ffffffffc"
}
],
"thread_metadata": {
"259": {
"priority": 31
}
}
},
}
debug_meta
- Object, required on native platforms. This carries the same payload as the
debug_meta
interface.It is required to have it for native platforms in order to symbolicate. For non-native platforms, you can omit this. device
- Object, required. This contains several fields that describe the device the profiler is running on.
It can have the following attributes:
architecture
: String, required.is_emulator
: Boolean, optional. Indicates if the application is running in an emulator. Usually only set for mobile platforms.locale
: String, optional. Contains the local of the system. Usually only set for a mobile platforms.manufacturer
: String, optional. Contains the manufacturer of the device. Usually only set for mobile platforms.model
: String, optional. Contains the model of the device. Usually only set for mobile platforms.
{
"device": {
"architecture": "arm64e",
"is_emulator": true,
"locale": "en_US",
"manufacturer": "Apple",
"model": "iPhone14,8"
}
}
environment
- String, recommended. The environment name, such as
production
orstaging
. The default value isproduction
. event_id
- String, required. Hexadecimal string representing a uuid4 value. The length must be exactly 32 characters. Dashes and uppercase letters are not allowed.
measurements
- Object, optional. This field contains various metrics measured during the profile collection.
A measurement is a collection of measurement values and a unit for those values. It will only be shown in the UI if it's been integrated. Right now, we only support 2 metrics:
frozen_frame_renders
: when a frozen frame was detected.slow_frame_renders
: when a slow frame was detected.
A measurement value can contain the following attributes:
elapsed_since_start_ns
: number of nanoseconds since the start of the profile (stored intimestamp
). It should be wall time.value
: value for the measurement, as afloat64
. We acceptfloat64
formatted asstring
as well.
These are the values accepted for unit
:
nanosecond
ns
hertz
hz
byte
percent
{
"cpu_0": {
"unit": "percent",
"values": {
"elapsed_since_start_ns: "1234567890",
"value": 12.64,
}
}
}
os
- Object, required. Contains information about the OS.
It can have the following attributes:
name
: String, required. Contains the OS name.version
: String, required. Contains the OS version.build_number
: String, optional. Only set it forcocoa
platform.
{
"os": {
"build_number": "20D47",
"name": "iOS",
"version": "16.3"
}
}
platform
- String, required. This value represents the platform the SDK is submitting from.
Acceptable values are:
cocoa
node
python
rust
profile
- Object, required. It contains all the raw data collected when profiling. See profile data.
release
- String, required. The release version of the application.
Release versions must be unique across all projects in your organization. This value can be the git SHA for the given project, or a product identifier with a semantic version (suggested format my-project-name@1.0.0
).
runtime
- Object, optional. Contains information about the runtime. Usually only used for platforms allowing several runtimes.
It can have the following attributes:
name
version
{
"runtime": {
"name": "CPython",
"version": "3.11.2"
}
}
timestamp
- String, recommended. The timestamp when the profile was captured by the SDKas RFC 3339 format. This is meant to bethe anchor for all relative timestamps captured for the profile.
Ideally, this will be the same as the field start_timestamp
on the transaction so all relative timestamps for transaction and profile can be both from the same reference point and we don't need to correct relative timestamps.
If for some reasons, the profiler implementation doesn't allow that, set it to the closest time to the profiler start (or to whatever the profiler gives you as a profile start timestamp) and it will be used to align with the transaction later on.
transaction
- Object, required. Contains information about the transaction it is associated with.
It can have the following attributes:
id
: String, required. Contains the transaction ID.name
: String, required. Contains the name of the transaction.trace_id
: String, required. Contains the trace ID the transaction is part of.active_thread_id
: String, required. Contains the thread ID the transaction was started on asuint64
converted to a string. On mobile, this is usually the main thread.
version
- String, required. Represents the version of the Sample format.
Acceptable values are:
"1"
The profile data consists of
- sample, which specifies the start time, their corresponding thread and a stack,
- stacks, which are lists of frames,
- frames, which identify the function and/or line of code for each item in a stacktrace,
- thread metadata, with info about threads.
The selected format is trying to keep the amount of data we transfer to the server as limited as possible.
{
"profile": {
"samples": [
{ ... },
...
],
"stacks": [
[ ... ],
...
],
"frames": [
{ ... },
...
],
"thread_metadata": {
"259": {
...
}
}
}
}
When collecting a stack trace, you will:
- Capture the stack trace.
- Capture a timestamp relative to the start of the profile (set in the
timestamp
field). - Capture the thread ID where this stack trace was captured.
- Create a list of indices for each frames (add it to the frame list if needed or use the existing frame's index).
- Check if that stack exists in the list of stacks and if it doesn't, add it to the list of stacks
- Create a sample containing the thread ID, the relative timestamp and the stack ID.
You should collect samples at a frequency of 101Hz, or roughly once every 10 milliseconds.
Note
The 101Hz number above is not a typo. It is intentionally slightly off from 100Hz to avoid a condition named "lockstep sampling" where the profiling samples occur at the same frequency as a loop in the application. Ideally, the samples should be much more frequent than any cycles in the application, or at random intervals, so that the chance it occurs in any particular operation is proportional to the amount of time that operation takes. But this is often not feasible, so the next best thing is to use a sampling rate that doesn't coincide with the likely frequency of program cycles.
We also chose 101 for its primality, whereas 1 below 100–99–is evenly divisible by several smaller numbers, which could lead to similar lockstep behavior.
This explanation is an excerpt from this awesome StackOverflow answer which has more details and a nice analogy for the issue.
samples
- List, required Contains a list of sample object captured during execution.Each sample can contain the following attributes:
elapsed_since_start_ns
: String, required. Contains the relative timestamp in nanoseconds since the value contained in thetimestamp
field when the sample was captured. It's meant to be auint64
formatted as astring
, afloat
won't be accepted. It should be wall time.stack_id
: Integer, required. Contains the stack index from thestacks
list.thread_id
: String, required. Contains the thread ID on which the sample was captured.
{
"elapsed_since_start_ns": "1234567890",
"stack_id": 1,
"thread_id": "259"
}
stacks
- List, required. Contains a list of frame indices forming a stack trace.
Frames in a stack should be ordered from leaf to root. It means your main frame should be at the end of the list.
It's encouraged to deduplicate stacks by only storing it once and referring to the same stack_id
for each sample that needs it.
frames
- List, required. Contains a list of frame objects (see Frame Attributes).
Each object should contain at least a filename
, function
or instruction_addr
attribute. All values are optional, but recommended.
For native platforms (cocoa
or rust
for example), instruction_addr
is required in order to be able to symbolicate and get more info about the frame.
For the rest of the platforms, whatever is passed here will travel to the frontend and whatever is available can be used to show more or less information.
We will use module
or package
(in that order) to group frames when needed.
Some attributes are not used at the moment:
raw_function
pre_context
post_context
stack_start
vars
addr_mode
platform
thread_metadata
- Object, required. Contains an object with a field for each thread ID detected during runtime.
This object can contain these attributes:
name
: name of the thread.priority
: the priority of the thread.
This information will be used on the flamechart to power the thread selector.
{
"thread_metadata": {
"259": {
"name": "com.apple.main-thread",
"priority": 31
}
}
}
We will reject a profile in Relay for several reasons:
- profile data is missing (no frame, no sample, no stack)
- not enough samples (we need at least 2 samples)
- no transaction associated with the profile
- any of the metadata required is missing
- size of the profile exceeds 50MB
- a profile exceeds 30 seconds (difference between last and first sample timestamp)
It is suggested to validate these conditions before sending the profile.
It's suggested to remove unnecessary data like thread data without samples from thread_metadata
.
After this payload is generated, serialize it as JSON, pack it into the same Envelope as the associated transaction with the item type profile
and send it to Relay.
This envelope should look like this:
{"event_id":"a229377b82ad4898be7c3a6272d052d9"}
{"type":"transaction"}
{ /* transaction JSON payload */ }
{"type":"profile"}
{ /* profile JSON payload */}
Our documentation is open source and available on GitHub. Your contributions are welcome, whether fixing a typo (drat!) or suggesting an update ("yeah, this would be better").