1
mutable struct CallFrame
2
    functionName::String
3
    scriptId::String # unique script identifier
4
    url::String
5
    lineNumber::Int64
6
    columnNumber::Int64
7
end
8

9
struct PositionTickInfo
10
    line::Int64
11
    ticks::Int64
12
end
13

14
"""
15
    ProfileNode
16

17
A `ProfileNode` represents a method in the call-graph. Each child in `children`
18
is a callee and the `positionTicks` represents the location.
19
"""
20
mutable struct ProfileNode
21
    id::Int64
22
    callFrame::CallFrame
23
    hitCount::Int64 
24
    children::Vector{Int64}
25
    positionTicks::Vector{PositionTickInfo}
26
end
27

28 0
function enter!(node::ProfileNode, line)
29 0
    idx = findfirst(tick->tick.line == line, node.positionTicks)
30 0
    if idx === nothing
31 0
        push!(node.positionTicks, PositionTickInfo(line, 1))
32
    else
33 0
        old = node.positionTicks[idx]
34 0
        node.positionTicks[idx] = PositionTickInfo(line, old.ticks + 1)
35
    end
36
end
37

38
struct CPUProfile
39
    nodes::Vector{ProfileNode}
40
    startTime::Int64
41
    endTime::Int64
42
    samples::Vector{Int64}
43
    timeDeltas::Vector{Int64}
44
end
45

46
"""
47
    CPUProfile(data, period)
48

49
Fetches the collected `Profile` data.
50

51
# Arguments:
52
- `data::Vector{UInt}`: The data provided by `Profile.fetch` [optional].
53
- `period::UInt64`: The sampling period in nanoseconds [optional].
54
"""
55

56 0
function CPUProfile(data::Union{Nothing, Vector{UInt}} = nothing,
57
                    period::Union{Nothing, UInt64} = nothing; from_c = false)
58 0
    if data === nothing
59 0
        data = copy(Profile.fetch())
60
    end
61 0
    lookup = Profile.getdict(data)
62 0
    if period === nothing
63 0
        period = ccall(:jl_profile_delay_nsec, UInt64, ())
64
    end
65

66 0
    period = period ÷ 1000 # ns to ms
67

68 0
    methods = Dict{Tuple{String, Int64}, CallFrame}()
69 0
    function get_callframe!(url, start_line, name, file)
70 0
        get!(methods, (url, start_line)) do
71 0
            CallFrame(name, file, url, start_line, 0)
72
        end
73
    end
74

75 0
    nodes = ProfileNode[]
76 0
    function insert_node!(callframe)
77 0
        node = ProfileNode(length(nodes) + 1, callframe, 0, Int64[], [])
78 0
        push!(nodes, node)
79 0
        return node
80
    end
81

82 0
    samples    = Int64[] #  Ids of samples leaf.
83 0
    toplevel   = Set{Int64}() #  Ids of samples top nodes.
84 0
    timeDeltas = Int64[] # Time intervals between adjacent samples in microseconds. The first delta is relative to the profile startTime.
85

86
    # start decoding backtraces
87 0
    lastwaszero = true
88 0
    current = nothing
89

90
    # data is in bottom-up order therefore reverse it
91 0
    for ip in Iterators.reverse(data)
92
        # ip == 0x0 is the sentinel value for finishing a backtrace, therefore finising a sample
93 0
        if ip == 0
94
            # Avoid creating empty samples
95 0
            if lastwaszero
96 0
                @assert current === nothing
97 0
                continue
98
            end
99 0
            lastwaszero = true
100

101 0
            if current !== nothing
102 0
                push!(samples, current.id)
103 0
                push!(timeDeltas, period)
104
            end
105

106 0
            current = nothing
107 0
            continue
108
        end
109 0
        lastwaszero = false
110

111
        # A backtrace consists of a set of IP (Instruction Pointers), each IP points
112
        # a single line of code and `litrace` has the necessary information to decode
113
        # that IP to a specific frame (or set of frames, if inlining occured).
114

115
        # decode the inlining frames
116 0
        for frame in Iterators.reverse(lookup[ip])
117
            # ip 0 is reserved
118 0
            frame.pointer == 0 && continue
119 0
            frame.from_c && !from_c && continue
120

121
            # A `frame` is an entry in a backtrace. A `ProfileNode` is a method in the call-graph
122
            # which can be hit by multiple frames. So first we need to go from frame to method.
123
            # As keys we will use `url` + `start_line`.
124 0
            if frame.linfo === nothing # inlined frame
125 0
                file = string(frame.file)
126 0
                name = string(frame.func)
127 0
                start_line = convert(Int64, frame.line) # TODO: Get start_line properly
128
            else
129 0
                if frame.linfo isa Core.MethodInstance
130 0
                    linfo = frame.linfo::Core.MethodInstance
131 0
                elseif frame.linfo isa Core.CodeInfo
132
                    # TODO: stackframes.jl describes this as a top-level frame
133 0
                    linfo = frame.linfo.parent::Core.MethodInstance
134
                end
135 0
                meth       = linfo.def
136 0
                file       = string(meth.file)
137 0
                name       = string(meth.module, ".", meth.name)
138 0
                start_line = convert(Int64, meth.line)
139
            end
140
            # TODO: Deal with unkown or unresolved functions
141 0
            url = Base.find_source_file(file)
142 0
            if url === nothing
143 0
                url = file
144
            end
145

146
            # We use callframe as a identifier
147 0
            callframe = get_callframe!(url, start_line, name, file)
148

149 0
            node = nothing
150 0
            if current === nothing # top-level
151
                # have we seen this callframe before?
152 0
                for id in toplevel
153 0
                    if nodes[id].callFrame === callframe
154 0
                        node = nodes[id]
155
                    end
156
                end
157 0
                if node === nothing
158 0
                    node = insert_node!(callframe)
159
                end
160 0
                node.hitCount += 1
161 0
                push!(toplevel, node.id)
162

163
            else
164
                # is this callframe present in our parent?
165 0
                for id in current.children
166 0
                    if nodes[id].callFrame === callframe
167 0
                        node = nodes[id]
168
                    end
169
                end
170 0
                if node === nothing
171 0
                    node = insert_node!(callframe)
172 0
                    push!(current.children, node.id)
173
                end
174
            end
175
            # mark the new line as hit
176 0
            enter!(node, frame.line)
177 0
            current = node
178
        end
179
    end
180

181 0
    if current !== nothing
182 0
        push!(samples, current.id)
183 0
        push!(timeDeltas, period)
184
    end
185

186 0
    start_time = 0
187 0
    timeDeltas[1] = 0
188 0
    @assert length(timeDeltas) == length(samples)
189 0
    return CPUProfile(nodes, start_time, start_time + sum(timeDeltas), samples, timeDeltas) 
190
end
191

192 0
function save_cpuprofile(filename, data::Union{Nothing, Vector{UInt}} = nothing,
193
                         period::Union{Nothing, UInt64} = nothing; kwargs...)
194

195 0
    profile = CPUProfile(data, period; kwargs...)
196 0
    open(filename, "w") do io
197 0
        write(io, '{')
198 0
        JSON.print(io, "nodes")
199 0
        write(io, ':')
200 0
        write(io, '[')
201 0
        for (i, node) in enumerate(profile.nodes)
202 0
            write(io, '{')
203 0
            JSON.print(io, "id")
204 0
            write(io, ':')
205 0
            JSON.print(io, node.id)
206 0
            write(io, ',')
207 0
            JSON.print(io, "callFrame")
208 0
            write(io, ':')
209 0
            write(io, '{')
210 0
                JSON.print(io, "functionName")
211 0
                write(io, ':')
212 0
                JSON.print(io, node.callFrame.functionName)
213 0
                write(io, ',')
214 0
                JSON.print(io, "scriptId")
215 0
                write(io, ':')
216 0
                JSON.print(io, node.callFrame.scriptId)
217 0
                write(io, ',')
218 0
                JSON.print(io, "url")
219 0
                write(io, ':')
220 0
                JSON.print(io, node.callFrame.url)
221 0
                write(io, ',')
222 0
                JSON.print(io, "lineNumber")
223 0
                write(io, ':')
224 0
                JSON.print(io, node.callFrame.lineNumber)
225 0
                write(io, ',')
226 0
                JSON.print(io, "columnNumber")
227 0
                write(io, ':')
228 0
                JSON.print(io, node.callFrame.columnNumber)
229 0
            write(io, '}')
230 0
            write(io, ',')
231 0
            JSON.print(io, "hitCount")
232 0
            write(io, ':')
233 0
            JSON.print(io, node.hitCount)
234 0
            write(io, ',')
235 0
            JSON.print(io, "children")
236 0
            write(io, ':')
237 0
            JSON.print(io, node.children)
238 0
            write(io, ',')
239 0
            JSON.print(io, "positionTicks")
240 0
            write(io, ':')
241 0
            write(io, '[')
242 0
            for (j, tick) in enumerate(node.positionTicks)
243 0
                write(io, '{')
244 0
                JSON.print(io, "line")
245 0
                write(io, ':')
246 0
                JSON.print(io, tick.line)
247 0
                write(io, ',')
248 0
                JSON.print(io, "ticks")
249 0
                write(io, ':')
250 0
                JSON.print(io, tick.ticks)
251 0
                write(io, '}')
252 0
                j == length(node.positionTicks) || write(io, ',')
253
            end
254 0
            write(io, ']')
255 0
            write(io, '}')
256 0
            i == length(profile.nodes) || write(io, ',')
257
        end
258 0
        write(io, ']')
259 0
        write(io, ',')
260 0
        JSON.print(io, "startTime")
261 0
        write(io, ':')
262 0
        JSON.print(io, profile.startTime)
263 0
        write(io, ',')
264 0
        JSON.print(io, "endTime")
265 0
        write(io, ':')
266 0
        JSON.print(io, profile.endTime)
267 0
        write(io, ',')
268 0
        JSON.print(io, "samples")
269 0
        write(io, ':')
270 0
        JSON.print(io, profile.samples)
271 0
        write(io, ',')
272 0
        JSON.print(io, "timeDeltas")
273 0
        write(io, ':')
274 0
        JSON.print(io, profile.timeDeltas)
275 0
        write(io, '}')
276
    end
277 0
    nothing
278
end

Read our documentation on viewing source code .

Loading