///|
/// Resolve aliases in one namespace; function and called-function IDs are shared.
fn name_value(
text : String,
names : Map[UInt64, String],
line : Int32,
) -> String raise ParseError {
if text.length() < 2 || text[0] != 40 || text[1] < 48 || text[1] > 57 {
return text
}
let mut end = 1
while end < text.length() && text[end] != 41 {
end = end + 1
}
if end == text.length() {
raise Syntax(line, "unterminated name ID")
}
let id = unsigned(text[1:end].to_owned(), line)
if end + 1 == text.length() {
match names.get(id) {
Some(value) => value
None => raise Syntax(line, "undefined name ID")
}
} else {
if !ws(text[end + 1]) {
raise Syntax(line, "name ID must be followed by whitespace")
}
let value = trim_ascii(text[end + 1:].to_owned())
if names.length() >= 100000 && !names.contains(id) {
raise Resource(line, "too many name IDs")
}
names.set(id, value)
value
}
}
///|
fn fields(
text : String,
line : Int32,
positions~ : Bool,
) -> Array[String] raise ParseError {
let items = words(text)
if items.is_empty() || items.length() > 64 {
raise Syntax(line, "expected 1..64 fields")
}
let seen : Map[String, Bool] = Map([])
let mut previous_position = -1
for item in items {
if seen.contains(item) {
raise Syntax(line, "duplicate field")
}
if positions {
let rank = match item {
"instr" => 0
"bb" => 1
"line" => 2
_ => raise Unsupported(line, "unknown position field: " + item)
}
if rank <= previous_position {
raise Syntax(line, "positions must follow instr, bb, line order")
}
previous_position = rank
}
seen.set(item, true)
}
items
}
///|
/// Parse the documented cost/call profile. Jumps and unknown body directives fail closed.
pub fn parse(text : String) -> Profile raise ParseError {
if text.length() > 16777216 {
raise Resource(0, "input exceeds 16 Mi UTF-16 code units")
}
let files : Map[UInt64, String] = Map([])
let functions : Map[UInt64, String] = Map([])
let objects : Map[UInt64, String] = Map([])
let parts : Array[Part] = []
let records : Array[Record] = []
let mut headers : Array[Header] = []
let mut events : Array[String] = []
let mut positions : Array[String] = ["line"]
let mut have_positions = false
let mut body = false
let mut trailer = false
let mut source = empty_context()
let mut have_function = false
let mut called : Context? = None
let mut called_file : String? = None
let mut called_object : String? = None
let mut pending : (UInt64, Context, Array[UInt64])? = None
let mut previous : Array[UInt64] = [0UL]
let mut line = 0
let mut expanded = 0
for view in text.split("\n") {
line = line + 1
if view.length() > 1048576 {
raise Resource(line, "line exceeds 1 Mi code units")
}
let row = trim_ascii(view.to_owned())
if row == "" || row.has_prefix("#") {
continue
}
let first = row[0]
let cost = (first >= 48 && first <= 57) ||
first == 43 ||
first == 45 ||
first == 42
if cost {
if trailer {
raise Syntax(line, "cost after totals trailer")
}
if events.is_empty() {
raise Syntax(line, "missing events header")
}
if !have_function {
raise Syntax(line, "cost without fn context")
}
let tokens = words(row)
let n = positions.length()
if tokens.length() < n || tokens.length() > n + events.length() {
raise Syntax(line, "wrong number of cost columns")
}
let pos : Array[UInt64] = []
for i = 0; i < n; i = i + 1 {
pos.push(position(tokens[i], previous[i], line))
}
previous = pos.copy()
let values : Array[UInt64] = []
for i = n; i < n + events.length(); i = i + 1 {
values.push(
if i < tokens.length() {
unsigned(tokens[i], line)
} else {
0UL
},
)
}
let kind = match pending {
None => SelfCost
Some((count, target, target_pos)) => Call(count, target, target_pos)
}
if records.length() >= 200000 {
raise Resource(line, "too many cost records")
}
let record : Record = {
part: parts.length(),
line,
context: source,
positions: pos,
values,
kind,
}
let size = record_size(record)
if size > 16777216 - expanded {
raise Resource(line, "expanded records exceed size budget")
}
expanded = expanded + size
records.push(record)
if pending is Some(_) {
called_file = None
called_object = None
}
pending = None
body = true
continue
}
if pending is Some(_) {
raise Syntax(line, "calls must be followed by a cost record")
}
let mut separator = 0
while separator < row.length() &&
row[separator] != 58 &&
row[separator] != 61 {
separator = separator + 1
}
if separator == row.length() {
raise Syntax(line, "expected header or position directive")
}
let key = trim_ascii(row[:separator].to_owned())
let value = trim_ascii(row[separator + 1:].to_owned())
if row[separator] == 58 {
if (body && key != "totals") ||
(key == "part" && headers.any(h => h.key == "part")) {
if events.is_empty() {
raise Syntax(line, "part without events")
}
if parts.length() >= 1023 {
raise Resource(line, "too many dump parts")
}
parts.push({ headers, events, positions, })
headers = []
events = []
positions = ["line"]
have_positions = false
previous = [0UL]
source = empty_context()
have_function = false
called = None
called_file = None
called_object = None
body = false
trailer = false
}
match key {
"events" => {
if !events.is_empty() {
raise Syntax(line, "repeated events header")
}
events = fields(value, line, positions=false)
}
"positions" => {
if have_positions {
raise Syntax(line, "repeated positions header")
}
positions = fields(value, line, positions=true)
previous = positions.map(_ => 0UL)
have_positions = true
}
"version" =>
if value != "1" {
raise Unsupported(line, "only Callgrind version 1 is supported")
}
"summary" | "totals" => {
if events.is_empty() {
raise Syntax(line, "counter header before events")
}
let totals = words(value)
if totals.length() > events.length() {
raise Syntax(line, "too many summary/totals columns")
}
for token in totals {
ignore(unsigned(token, line))
}
if key == "totals" {
trailer = true
}
}
_ => ()
}
if headers.length() >= 4096 {
raise Resource(line, "too many headers in dump part")
}
headers.push({ key, value, line, })
continue
}
if trailer {
raise Syntax(line, "position directive after totals trailer")
}
match key {
"ob" => source = { ..source, object: name_value(value, objects, line), }
"fl" => {
let file = name_value(value, files, line)
source = { ..source, cost_file: file, }
}
"fi" | "fe" =>
source = { ..source, cost_file: name_value(value, files, line), }
"fn" => {
source = {
..source,
file: source.cost_file,
function_name: name_value(value, functions, line),
}
have_function = true
called = None
}
"cob" => called_object = Some(name_value(value, objects, line))
"cfl" | "cfi" => called_file = Some(name_value(value, files, line))
"cfn" => {
let object = match called_object {
Some(v) => v
None => source.object
}
let file = match called_file {
Some(v) => v
None => source.cost_file
}
called = Some({
object,
file,
cost_file: file,
function_name: name_value(value, functions, line),
})
}
"calls" => {
let tokens = words(value)
if tokens.length() != positions.length() + 1 {
raise Syntax(line, "wrong number of call target positions")
}
let target = match called {
None => raise Syntax(line, "calls without cfn context")
Some(v) => v
}
let target_pos : Array[UInt64] = []
for i = 0; i < positions.length(); i = i + 1 {
target_pos.push(position(tokens[i + 1], previous[i], line))
}
pending = Some((unsigned(tokens[0], line), target, target_pos))
}
_ => raise Unsupported(line, "unsupported body directive: " + key)
}
}
if pending is Some(_) {
raise Syntax(line, "incomplete call record at end of input")
}
if events.is_empty() {
raise Syntax(line, "missing events header")
}
parts.push({ headers, events, positions, })
{ parts, records, }
}