# Please refer to the COPYRIGHT file of the profiling package for details. # SPDX-License-Identifier: MIT # # profiling: Line by line profiling and code coverage for GAP # # Implementations #
InstallGlobalFunction( "ReadLineByLineProfile", function(filename)
local res, stacks; if IsLineByLineProfileActive() then
Info(InfoWarning, 1, "Reading Profile while still generating it!"); fi;
res := READ_PROFILE_FROM_STREAM(UserHomeExpand(filename), 0);
return res; end );
# Merges a full list of profiles -- can run out of memory for many profiles.
BindGlobal("_prof_mergeProfiles", function(filenames)
local profs, f, outprof, p, line, file, line_info, line_function_calls,
stacks, unionlist, linefunccpy, i;
if Size(filenames) = 0then
ErrorNoReturn("Filenames list must be non-empty"); fi;
profs := []; # First turn all filenames into profiles for f in filenames do if IsRecord(f) then
Add(profs, f); else
Add(profs, ReadLineByLineProfile(f)); fi;
od;
if not ForAll(profs, x -> x.info.is_cover = profs[1].info.is_cover) then
ErrorNoReturn("Some profiles are covers, some are time profiles"); fi;
if not ForAll(profs, x -> x.info.time_type = profs[1].info.time_type) then
ErrorNoReturn("Some profiles use wall time, some use CPU time"); fi;
outprof := rec();
outprof.info := profs[1].info;
# merge runtimes
stacks := DictionaryBySort(true); for p in profs do for line in p.stack_runtimes do if KnowsDictionary(stacks, line[1]) then
AddDictionary(stacks, line[1], LookupDictionary(stacks, line[1]) + line[2]); else
AddDictionary(stacks, line[1], line[2]); fi;
od;
od; # Woo, internal datastructure abuse
outprof.stack_runtimes := stacks!.entries;
line_info := DictionaryBySort(true); for p in profs do for file in p.line_info do if KnowsDictionary(line_info, file[1]) then
AddDictionary(line_info, file[1],
file[2] + LookupDictionary(line_info, file[1])); else
AddDictionary(line_info, file[1], file[2]); fi;
od;
od; # Woo, internal datastructure abuse
outprof.line_info := line_info!.entries;
line_function_calls := DictionaryBySort(true); for p in profs do for file in p.line_function_calls do
if KnowsDictionary(line_function_calls, file[1]) then
unionlist := [];
linefunccpy := LookupDictionary(line_function_calls, file[1]); for i in [1..Maximum(Length(file[2]), Length(linefunccpy))] do if IsBound(file[2][i]) and IsBound(linefunccpy[i]) then
unionlist[i] := Union(file[2][i], linefunccpy[i]); elif IsBound(file[2][i]) then
unionlist[i] := Set(file[2][i]); else
unionlist[i] := Set(linefunccpy[i]); fi;
od;
AddDictionary(line_function_calls, file[1], unionlist); else
AddDictionary(line_function_calls, file[1], file[2]); fi;
InstallGlobalFunction( "MergeLineByLineProfiles", function(filenames)
local ret, prof, f;
if Size(filenames) = 0then
ErrorNoReturn("Filenames list must be non-empty"); fi;
ret := fail;
# Merge in pairs, else we can run out of memory. for f in filenames do if IsRecord(f) then
prof := f; else # First turn all filenames into profiles
prof := ReadLineByLineProfile(f); fi;
if ret = fail then
ret := prof; else
ret := _prof_mergeProfiles([ret, prof]); fi;
od;
return ret; end);
# This internal function just pretty prints a function object
_Prof_PrettyPrintFunction := function(f)
return Concatenation(f.name, "@", f.filename, ":", String(f.line)); end;
# This just makes it easy to give dictionaries a default value
_prof_LookupWithDefault := function(dict, val, default)
local v;
v := LookupDictionary(dict, val); if v = fail then
return default; else
return v; fi; end;
# Returns a list of records containing: # [function, time in func+children, time in func, calls]
_Prof_GatherFunctionUsage := function(data)
local funccollection, trace, lastfunc, funcset, pos, f;
if not(IsRecord(data)) then
data := ReadLineByLineProfile(data); fi;
funccollection := [];
for trace in data.stack_runtimes do if(Length(trace[1]) > 0) then
lastfunc := trace[1][Length(trace[1])];
funcset := Set(trace[1]); for f in funcset do # Use the fact that [f,0,0,0] will be put in the same place as [f,x,y] for x>=0, y>=0
pos := PositionSorted(funccollection, [f, 0, 0, 0]); if Length(funccollection) < pos or funccollection[pos][1] <> f then
AddSet(funccollection, [f, 0, 0, 0]);
pos := PositionSet(funccollection, [f, 0, 0, 0]); fi;
funccollection[pos][2] := funccollection[pos][2] + trace[2]; if f = lastfunc then
funccollection[pos][3] := funccollection[pos][3] + trace[2];
funccollection[pos][4] := funccollection[pos][4] + 1; fi;
od; fi;
od;
return funccollection; end;
InstallGlobalFunction("OutputFlameGraphInput",function(args...)
local outstream, trace, fun, firstpass, data, filename, retstring; if Length(args) < 1 or Length(args) > 2then
ErrorNoReturn("Usage: OutputFlameGraph(cover[, filename])"); fi;
data := args[1];
if Length(args) = 2then
outstream := OutputTextFile(args[2], false); if outstream = fail then
ErrorNoReturn("Unable to write to file ", outstream); fi; else
retstring := "";
outstream := OutputTextString(retstring, false); fi;
SetPrintFormattingStatus(outstream, false);
if not(IsRecord(data)) then
data := ReadLineByLineProfile(data); fi;
for trace in data.stack_runtimes do
firstpass := true; for fun in trace[1] do if firstpass = true then
firstpass := false; else
PrintTo(outstream, ";"); fi;
PrintTo(outstream, _Prof_PrettyPrintFunction(fun));
od;
PrintTo(outstream, " ", String(trace[2]), "\n");
od;
CloseStream(outstream);
if IsBound(retstring) then
return retstring; fi; end);
InstallGlobalFunction("OutputFlameGraph", function(args...)
local instr, instream, outstr, outstream, returnstring, options, extraarg;
# Try to make directory (might already exist)
IO_mkdir(outdir, IO.S_IRUSR+IO.S_IWUSR+IO.S_IXUSR+
IO.S_IRGRP+IO.S_IXGRP+
IO.S_IROTH+IO.S_IXOTH);
if IO_opendir(outdir) = fail then
ErrorNoReturn("Unable to access directory ", outdir); fi;
IO_closedir();
if not(IsRecord(data)) then
data := ReadLineByLineProfile(data); fi;
warnedExecNotRead := false;
# Don't bother warning about missing 'read' lines if we are just profiling if data.info.is_cover = false then
warnedExecNotRead := true; fi;
# IntegerToString with insertion of thousand-separators
stringWithSeparators := function( n )
local i, j, str, withSeps;
str := Reversed( String(n) );
withSeps := "";
j := 0; for i in [1..Length(str)] do
withSeps[i+j] := str[i]; if i mod 3 = 0 and i < Length(str) then
withSeps[ i+j+1 ] := ',';
j := j+1; fi;
od;
return Reversed( withSeps ); end;
outputhtmlhead := function(outstream)
PrintTo(outstream, "<!DOCTYPE html><script src=\"sorttable.js\"></script><html>\n");
PrintTo(outstream, "<head><title>\n"); if IsBound(options.title) then
PrintTo(outstream, options.title); fi;
PrintTo(outstream, "</title></head>\n"); end;
outputfunctablehtml := function(outstream)
local funcusage, line, fn, linkname,name;
if not hasCoverage then
PrintTo(outstream, "<p>This file was read by GAP before profiling was actived, so lines which were not read but not executed are not marked.</p>"); fi;
PrintTo(outstream, "<thead>");
PrintTo(outstream, "<tr>"); if hasTiming then
PrintTo(outstream, "<th>Line</th><th>Execs</th><th>Time</th><th>Time+Childs</th><th>Code</th><th>Called Functions</th><th>Called From</th>\n"); else
PrintTo(outstream, "<th>Line</th><th>Code</th>\n"); fi;
PrintTo(outstream, "</tr>");
PrintTo(outstream, "</thead>\n");
PrintTo(outstream, "<tbody>\n"); for i in [1..Length(lines)] do
if not(IsBound(coverage[i])) or (coverage[i] = [0,0,0,0]) then
outchar := "ignore"; elif coverage[i][2] >= 1then
outchar := "exec"; elif coverage[i][1] >= 1then
outchar := "missed"; else
Error("Invalid profile - there were lines which were not executed, but took time!"); fi;
if hasTiming then
time := "<td></td><td></td><td></td>"; if IsBound(coverage[i]) and coverage[i][2] >= 1then
calls := coverage[i][2]; if data.info.is_cover and calls > 1then
calls := 0; fi;
if coverage[i][3] >= 1 or coverage[i][4] >= 1then
time := Concatenation("<td>",stringWithSeparators(calls), "</td><td>",
stringWithSeparators(coverage[i][3]), "</td><td>",
stringWithSeparators(coverage[i][4]+coverage[i][3]), "</td>"); else
time := Concatenation("<td>",stringWithSeparators(calls), "</td><td></td><td></td>"); fi; fi;
if hasTiming then # totaltime := LookupWithDefault(linedict.recursetime, i, "");
calledfns := ""; if Length(subfunctions) >= i then for fn in subfunctions[i] do
linkname := ReplacedString(fn.filename, "/", "_");
Append(linkname, ".html");
name := fn.name; if name = "nameless"then
name := Concatenation(fn.filename, ":", String(fn.line)); fi;
Append(calledfns, Concatenation("<a href=\"",linkname,"#line",String(fn.line),"\">",name,"</a> "));
od; fi;
PrintTo(outstream, "<td><span>",calledfns,"</span></td>");
calledfns := ""; if Length(calledbyfunctions) >= i then for fn in calledbyfunctions[i] do
linkname := ReplacedString(fn.filename, "/", "_");
Append(linkname, ".html");
name := Concatenation(fn.filename, ":", String(fn.line));
Append(calledfns, Concatenation("<a href=\"",linkname,"#line",String(fn.line),"\">",name,"</a> "));
od; fi;
PrintTo(outstream, "<td><span>",calledfns,"</span></td>"); fi;
PrintTo(outstream, "</tr>\n");
od;
PrintTo(outstream, "<tbody>\n"); for i in overview do
PrintTo(outstream, "<tr>");
PrintTo(outstream, "<td><a href='",
Remove(SplitString(i.outname,"/")), "'>",i.inname,"</a></td>");
if IsBound(i.execlines) and IsBound(i.readnotexeclines) then
codecover := 1 - (i.readnotexeclines / (i.execlines + i.readnotexeclines));
PrintTo(outstream, "<td class='coverage",Int(codecover*10),"0'>",Int(codecover*100),"</td>"); else
PrintTo(outstream, "<td>N/A</td>"); fi;
PrintTo(outstream, "<td>", stringWithSeparators(i.execlines), "</td>"); if IsBound(i.readnotexeclines) then
PrintTo(outstream, "<td>",
stringWithSeparators(i.execlines + i.readnotexeclines), "</td>"); else
PrintTo(outstream, "<td>?</td>"); fi;
if any_timeexec then if IsBound(i.filetime) and IsBound(i.fileexec) then
PrintTo(outstream, "<td>",
stringWithSeparators(i.filetime), "</td><td>",
stringWithSeparators(i.fileexec), "</td>"); else
PrintTo(outstream, "<td>N/A</td><td>N/A</td>"); fi; fi;
PrintTo(outstream, "</tr>\n");
od;
overview := []; for filenum in [1..Length(data.line_info)] do
fileinfo := data.line_info[filenum];
callinfo := data.line_function_calls[filenum];
calledbyinfo := data.line_calling_function_calls[filenum];
infile := fileinfo[1]; if Length(indir) <= Length(infile)
and indir = infile{[1..Length(indir)]} then # Make a nicer output filename, handling the input being in # directories, or having *s in the name.
outname := infile;
outname := ReplacedString(outname, "/", "_");
outname := ReplacedString(outname, "*", "_");
outname := Concatenation(outdir, "/", outname);
outname := Concatenation(outname, ".html");
outstream := OutputTextFile(outname, false);
SetPrintFormattingStatus(outstream, false);
# Check file exists. This also handles us accidentally trying # to open files like *stdin* if IsExistingFile(infile) then
instream := InputTextFile(infile);
allLines := [];
line := ReadLine(instream); while line <> fail do
Add(allLines, line);
line := ReadLine(instream);
od;
CloseStream(instream); else
allLines := List([1..Length(fileinfo[2])], x -> "<missing file>"); fi;
# Check for lines which are executed, but not read
if ForAny(fileinfo[2], x -> (x[1] = 0 and x[2] > 0) and not warnedExecNotRead) then
Print("# Warning: Some lines marked executed but not read. If you\n", "# want to see which lines are NOT executed,\n", "# use the --prof/--cover command line options\n");
warnedExecNotRead := true; fi;
if ForAny(overview, x -> IsBound(x.filetime) and x.filetime > 0) then for o in ["default", "reverse"] do for squash in ["standard", "squash"] do
flameoptions := rec(type := o); if squash = "standard"then
flameoptions.squash := false; else
flameoptions.squash := true; fi;
OutputFlameGraph(data, StringFormatted("{}/flame-{}-{}.svg", outdir, o, squash), flameoptions);
od;
od;
# HACK: provide backwards compatibility with old signature "data, outfile, jobid, pathtoremove" # To be removed once GAP master and stable-4.10 have switched to the new calling # conventions if IsString(opt) then # "pathtoremove" contains "jobid", "opt" contains the real "pathtoremove"; # we ignore the "jobid and
pathtoremove := opt;
Unbind(opt); fi; fi;
env := GAPInfo.SystemEnvironment; if not IsBound(opt) then if IsBound(env.TRAVIS) then
opt := rec(
service_name := "travis-ci",
service_job_id := env.TRAVIS_JOB_ID,
service_branch := env.TRAVIS_BRANCH,
); if IsBound(env.TRAVIS_PULL_REQUEST) and Length(env.TRAVIS_PULL_REQUEST) > 0then
opt.service_pull_request := env.TRAVIS_PULL_REQUEST; fi; elif IsBound(env.APPVEYOR) then
opt := rec(
service_name := "appveyor",
service_number := env.APPVEYOR_BUILD_VERSION,
service_branch := env.APPVEYOR_REPO_BRANCH,
commit_sha := env.APPVEYOR_REPO_COMMIT,
service_build_url := Concatenation( "https://ci.appveyor.com/project/",
env.APPVEYOR_REPO_NAME, "/build/",
env.APPVEYOR_BUILD_VERSION),
); if IsBound(env.APPVEYOR_PULL_REQUEST_NUMBER) then
opt.service_pull_request := env.APPVEYOR_PULL_REQUEST_NUMBER; fi; else
Error("Failed to detect your CI service, please specify via <opt>"); fi; fi;
if IsBound(env.COVERALLS_REPO_TOKEN) then
opt.repo_token := env.COVERALLS_REPO_TOKEN; fi;
if IsBound(env.COVERALLS_PARALLEL) and env.COVERALLS_PARALLEL <> "false"then
opt.parallel := "true"; fi;
if IsBound(env.COVERALLS_FLAG_NAME) then
opt.flag_name := env.COVERALLS_FLAG_NAME; fi;
# GAP's ReplacedString does not terminate for # empty string to replace if pathtoremove = ""then
processfilename := IdFunc; else
processfilename := fn -> ReplacedString(fn, pathtoremove, ""); fi;
IO_Write(outstream, "{\n"); for key in RecNames(opt) do
IO_Write(outstream, Concatenation("\"", key, "\": \"", opt.(key), "\",\n"));
od;
IO_Write(outstream, "\"source_files\": [\n");
prev := false;
for file in data.line_info do if IsExistingFile(file[1]) then if prev then
IO_Write(outstream, ",\n"); fi;
IO_Write(outstream, "{\n");
IO_Write(outstream, Concatenation( "\"name\": \""
, processfilename(file[1])
, "\",\n" ));
IO_Write(outstream, Concatenation("\"source_digest\": \""
, MD5File(file[1]) ,"\",\n"));
IO_Write(outstream, "\"coverage\": [");
# Use a temporary check to support GAP versions without ARCH_IS_WSL
__profiling_pkg_temp_wsl_check := function()
local bash, val; if IsBoundGlobal("ARCH_IS_WSL") then
return ValueGlobal("ARCH_IS_WSL")(); fi;
# Can't copy ARCH_IS_WSL from GAP core, as it requires GAPInfo.KernelInfo.uname
bash := Filename(DirectoriesSystemPrograms(), "bash"); if bash = fail then
return false; fi;
val := Process(Directory("/"), bash, InputTextNone(), OutputTextNone(), ["which", "explorer.exe"]);
return val = 0; end;
InstallGlobalFunction("LineByLineProfileFunction", function(f, args)
local dir;
if IsLineByLineProfileActive() then
ErrorNoReturn("Cannot profile when profiling already active!"); fi;
dir := DirectoryTemporary();
ProfileLineByLine(Filename(dir, "prof.gz"));
CallFuncList(f, args);
UnprofileLineByLine();
OutputAnnotatedCodeCoverageFiles(Filename(dir, "prof.gz"),
Filename(dir, "output")); if ARCH_IS_MAC_OS_X() then
Exec(Concatenation("open ",Filename(dir, "output/index.html"))); elif ARCH_IS_WINDOWS() then
Exec(Concatenation("cmd /c start ",Filename(dir, "output/index.html"))); # Support versions of GAP without this function elif __profiling_pkg_temp_wsl_check() then
Exec(Concatenation("explorer.exe \"$(wslpath -a -w \"",Filename(dir, "output/index.html"), "\")\"")); else
Exec(Concatenation("xdg-open ",Filename(dir, "output/index.html"))); fi; end);
InstallGlobalFunction("ProfileFile", function(testfile, args...)
local opts, indir, showOutput, open, rnam, rawfile, redirect, len, gap_cmd,
cmd, x, page; # Get options
opts := rec(outdir := DirectoryTemporary(),
indir := "",
showOutput := true,
open := true); if Length(args) = 1then if not IsRecord(args[1]) then
ErrorNoReturn("ProfileFile: <opts> must be a record"); fi; for rnam in RecNames(args[1]) do
opts.(rnam) := args[1].(rnam);
od; elif Length(args) > 1then
ErrorNoReturn("ProfileFile: takes 1 or 2 arguments, but ",
Length(args) + 1, " were given"); fi;
# Process profile
x := ReadLineByLineProfile(rawfile);;
OutputAnnotatedCodeCoverageFiles(x, opts.indir, opts.outdir);
page := Filename(opts.outdir, "index.html");
# Open page if opts.open = true then if ARCH_IS_MAC_OS_X() then
Exec(Concatenation("open ", page)); elif ARCH_IS_WINDOWS() then
Exec(Concatenation("cmd /c start ", page)); elif __profiling_pkg_temp_wsl_check() then
Exec(Concatenation("explorer.exe \"$(wslpath -a -w \"",page, "\")\"")); else
Exec(Concatenation("xdg-open ", page)); fi; fi;
return page; end);
InstallGlobalFunction("ProfilePackage", function(pkg_name, args...)
local info, dir, testfile, opts, rnam; # Check input if Length(args) = 0then
args := [rec()]; elif Length(args) > 1then
ErrorNoReturn("ProfilePackage: takes 1 or 2 arguments, but ",
Length(args) + 1, " were given"); fi;
# Get test location from package info
info := PackageInfo(pkg_name); if Length(info) = 0then
return fail; fi; if not IsBound(info[1].TestFile) then
ErrorNoReturn("ProfilePackage: no test file specified in package"); fi;
dir := info[1].InstallationPath;
testfile := Filename(Directory(dir), info[1].TestFile); if Length(info) >= 2then
Info(InfoWarning, 1, "ProfilePackage: \"", pkg_name, "\" installed in two locations");
Info(InfoWarning, 1, "ProfilePackage: using ", dir, " . . ."); fi;
# Call ProfileFile with the correct options
opts := rec(indir := dir); for rnam in RecNames(args[1]) do
opts.(rnam) := args[1].(rnam);
od;
return ProfileFile(testfile, opts); end);
# Outputs Lcov output
InstallGlobalFunction(OutputLcovCoverage, function(data, outfile)
local outstream, i, file, lines;
if not(IsRecord(data)) then
data := ReadLineByLineProfile(data); fi;
for file in data.line_info do if IsExistingFile(file[1]) then
IO_Write(outstream, "TN:\n");
IO_Write(outstream, Concatenation("SF:",file[1],"\n"));
lines := file[2]; for i in [1..Length(lines)] do if lines[i][1] > 0 or lines[i][2] > 0then
IO_Write(outstream, "DA:",i,",",lines[i][2],"\n"); fi;
od;
IO_Write(outstream, "end_of_record\n"); fi;
od;
IO_Close(outstream); end);
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.