Quellcodebibliothek Statistik Leitseite products/sources/formale Sprachen/GAP/pkg/profiling/gap/   (Algebra von RWTH Aachen Version 4.15.1©)  Datei vom 21.5.2025 mit Größe 35 kB image not shown  

Quelle  profiling.gi   Sprache: unbekannt

 
#  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) = 0 then
    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;

    od;
  od;
  outprof.line_function_calls := line_function_calls!.entries;

  return outprof;
end );

InstallGlobalFunction( "MergeLineByLineProfiles",
function(filenames)
  local ret, prof, f;

  if Size(filenames) = 0 then
    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) > 2 then
    ErrorNoReturn("Usage: OutputFlameGraph(cover[, filename])");
  fi;

  data := args[1];

  if Length(args) = 2 then
    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;

  returnstring := false;
  options := rec(type := "default");

  if Length(args) = 1 or (Length(args) = 2 and IsRecord(args[2])) then
    instr := OutputFlameGraphInput(args[1]);
    instream := InputTextString(instr);

    outstr := "";
    outstream := OutputTextString(outstr, false);
    returnstring := true;

    if Length(args) = 2 then
      options := args[2];
    fi;
  elif Length(args) = 2 or Length(args) = 3 then
    OutputFlameGraphInput(args[1], Concatenation(args[2], ".tmp"));
    instream := InputTextFile(Concatenation(args[2], ".tmp"));

    outstream := OutputTextFile(args[2], false);

    if Length(args) = 3 then
      options := args[3];
    fi;
  else
    ErrorNoReturn("OutputFlameGraph(profile [, filename] [,options])");
  fi;

  args := Filename(DirectoriesPackageLibrary( "profiling", "FlameGraph" ),"flamegraph.pl");
  if not IsBound(options.type) or options.type = "default" then
    ; # No argument needed
  elif options.type = "reverse" then
    args := Concatenation(args, " --reverse");
  else
    ErrorNoReturn("Invalid options.type in FlameGraph config: ", options.type);
  fi;

  if IsBound(options.squash) and options.squash then
    args := Concatenation(Filename(DirectoriesPackageLibrary( "profiling", "FlameGraph" ),"stackcollapse-recursive.pl"), " | ", args);
  fi;

  Process(DirectoryCurrent(), Filename(Directory("/bin"),"sh"),
          instream, outstream, ["-c", args]
         );

  if returnstring then
    return outstr;
  fi;
end);


# The CSS we want to inject into every page
_prof_CSS_std :=
"""
table { border-collapse: collapse }
tr:nth-child(odd)  { background-color: #EEE; }
tr:nth-child(even)  { background-color: #FFF; }
tr:nth-child(odd).exec  { background-color: #3E3; }
tr:nth-child(even).exec  { background-color: #6E6; }
tr:nth-child(odd).missed  { background-color: #F00; }
tr:nth-child(even).missed  { background-color: #F33; }
td, th {
    border: 1px solid #98bf21;
    padding: 3px 7px 2px 7px;
}
th {
    font-size: 1.1em;
    text-align: left;
    padding-top: 5px;
    padding-bottom: 4px;
    background-color: #A7C942;
    color: #ffffff;
}
table.sortable th:not(.sorttable_sorted):not(.sorttable_sorted_reverse):not(.sorttable_nosort):after {
    content: " \25B4\25BE"
}

/* HSV gradient made using http://www.perbang.dk/rgbgradient/ */
td.coverage00 { background-color: #FF0000; }
td.coverage10 { background-color: #F83100; }
td.coverage20 { background-color: #F25F00; }
td.coverage30 { background-color: #EB8B00; }
td.coverage40 { background-color: #E5B500; }
td.coverage50 { background-color: #DFDC00; }
td.coverage60 { background-color: #B0D800; }
td.coverage70 { background-color: #81D200; }
td.coverage80 { background-color: #55CB00; }
td.coverage90 { background-color: #2CC500; }
td.coverage100 { background-color: #04BF00; }""";

# handle the alignment of columns containing numbers
_prof_CSS_overview :=
"""
td:nth-child(2) { text-align: right; }
td:nth-child(3) { text-align: right; }
td:nth-child(4) { text-align: right; }
td:nth-child(5) { text-align: right; }
td:nth-child(6) { text-align: right; }
""";

_prof_CSS_files_withTiming :=
"""
td:nth-child(1) { text-align: right; }
td:nth-child(2) { text-align: right; }
td:nth-child(3) { text-align: right; }
td:nth-child(4) { text-align: right; }
""";

_prof_CSS_files_withoutTiming :=
"""
td:nth-child(1) { test-align: right; }
""";


# Checks if a file has correct coverage
_prof_fileHasCoverage := fileinfo -> not ForAny(fileinfo[2], x -> (x[1] = 0 and x[2] > 0));

# Checks if file has any timing attached to it
_prof_fileHasTiming := fileinfo -> ForAny(fileinfo[2], x -> (x[3] > 0));


##
InstallGlobalFunction("OutputAnnotatedCodeCoverageFiles",function(arg)
    local data, indir, outdir,
          infile, outname, instream, outstream, line, allLines,
          counter, overview, i, fileinfo, filenum, callinfo, calledbyinfo,
          readlineset, execlineset, outchar,
          outputhtml, outputoverviewhtml, outputfunctablehtml, outputhtmlhead,
          stringWithSeparators,
          warnedExecNotRead, filebuf, fileview, flame, options, flameoptions, o, squash;

    options := rec();

    data := arg[1];
    if Length(arg) = 2 then
      indir := "";
      outdir := arg[2];
    elif Length(arg) = 3 and not IsRecord(arg[3]) then
      indir := arg[2];
      outdir := arg[3];
    elif Length(arg) = 3 and IsRecord(arg[3]) then
      indir := "";
      outdir := arg[2];
      options := arg[3];
    elif Length(arg) = 4 and IsRecord(arg[4]) then
      indir := arg[2];
      outdir := arg[3];
      options := arg[4];
    else
      ErrorNoReturn("Usage: OutputAnnotatedCodeCoverageFiles(data, [indir,] outdir [, options])");
    fi;

    if IsDirectory(indir) then
      indir := indir![1];
    fi;

    if IsDirectory(outdir) then
      outdir := outdir![1];
    fi;

    indir := UserHomeExpand(indir);
    outdir := UserHomeExpand(outdir);

    # 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;

      funcusage := _Prof_GatherFunctionUsage(data);
      outputhtmlhead(outstream);
      PrintTo(outstream, "<body>\n");
      PrintTo(outstream, "<style>");
      PrintTo(outstream, _prof_CSS_std);
      PrintTo(outstream,"</style>");
      PrintTo(outstream, "<table class=\"sortable\">\n");

      PrintTo(outstream, "<thead>");
      PrintTo(outstream, "<tr>");
      PrintTo(outstream, "<th>Func</th><th>Execs</th><th>Time</th><th>Time+Childs</th>\n");
      PrintTo(outstream, "</tr>");
      PrintTo(outstream, "</thead>\n");

      PrintTo(outstream, "<tbody>\n");
      for line in funcusage do
        fn := line[1];
        PrintTo(outstream, "<tr><td>");
        linkname := ReplacedString(fn.filename, "/", "_");
        Append(linkname, ".html");
        name := fn.name;
        if name = "nameless" then
          name := Concatenation(fn.filename, ":", String(fn.line));
        fi;
        PrintTo(outstream, "<a href=\"",linkname,"#line",String(fn.line),"\">",name,"</a> ");
        PrintTo(outstream, "</td><td>",line[4], "</td><td>", line[3], "</td><td>", line[2], "</td></tr>\n");
      od;
      PrintTo(outstream, "</tbody></table></body></html>\n");
    end;

    outputhtml := function(lines, fileinfo, subfunctions, calledbyfunctions, outfilestream)
      local i, outchar, time, calls, calledfns, linkname, fn, name, filebuf, coverage, hasTiming, hasCoverage, funcs, outstream, outstring;
      outstring := "";
      outstream := OutputTextString(outstring, false);
      SetPrintFormattingStatus(outstream, false);

      hasTiming := _prof_fileHasTiming(fileinfo);
      hasCoverage := _prof_fileHasCoverage(fileinfo);
      coverage := fileinfo[2];
      outputhtmlhead(outstream);
      PrintTo(outstream, "<body>\n");
      PrintTo(outstream, "<style>");
      PrintTo(outstream, _prof_CSS_std);
      if hasTiming then
        PrintTo(outstream, _prof_CSS_files_withTiming);
      else
        PrintTo(outstream, _prof_CSS_files_withoutTiming);
      fi;
      PrintTo(outstream, "</style>");

      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, "<table class=\"sortable\">\n");

      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] >= 1 then
          outchar := "exec";
        elif coverage[i][1] >= 1 then
          outchar := "missed";
        else
          Error("Invalid profile - there were lines which were not executed, but took time!");
        fi;

        # Print start of page
        PrintTo(outstream, "<tr class='", outchar,"'>");
        PrintTo(outstream, "<td><a name=\"line",i,"\"></a>",i,"</td>");

        if hasTiming then
            time := "<td></td><td></td><td></td>";
            if IsBound(coverage[i]) and coverage[i][2] >= 1 then
              calls := coverage[i][2];
              if data.info.is_cover and calls > 1 then
                calls := 0;
              fi;

              if coverage[i][3] >= 1 or coverage[i][4] >= 1 then
                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;

            PrintTo(outstream, time);

        fi;

        PrintTo(outstream, "<td><span><tt>", HTMLEncodeString(lines[i]), "</tt></span></td>");

        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");
      PrintTo(outstream, "</table></body></html>\n");

      CloseStream(outstream);
      PrintTo(outfilestream, outstring);
    end;

    outputoverviewhtml := function(overview, outdir, havetime)
      local filename, outstream, codecover, i, any_timeexec;

      Sort(overview, function(v,w) return v.inname < w.inname; end);

      any_timeexec := ForAny(overview, i -> IsBound(i.filetime) and IsBound(i.fileexec) );

      filename := Concatenation(outdir, "/index.html");
      outstream := OutputTextFile(filename, false);
      SetPrintFormattingStatus(outstream, false);
      outputhtmlhead(outstream);
      PrintTo(outstream, "<body>\n");
      PrintTo(outstream, "<style>");
      PrintTo(outstream, Concatenation(_prof_CSS_std, _prof_CSS_overview));
      PrintTo(outstream, "</style>");
      if havetime then
        PrintTo(outstream, """
                          <table style="width:100%">
                          <tr>
                            <th><b>Flame Graphs</b>
                            <th>Standard</th>
                            <th>Stacks Reversed</th>
                          </tr>
                          <tr>
                            <th>Standard</th>
                            <td><a href="flame-default-standard.svg">Graph</a></th>
                            <td><a href="flame-reverse-standard.svg">Graph</a></tdt>
                          <tr>
                            <th>Squashed Recursive Function</th>
                            <td><a href="flame-default-squash.svg">Graph</a></th>
                            <td><a href="flame-reverse-squash.svg">Graph</a></td>
                          </tr>
                        </table></p>""");
        PrintTo(outstream, """<p><a href="funcoverview.html">Function Overview</a></p>""");
      fi;
      PrintTo(outstream, "<table cellspacing='0' cellpadding='0' class=\"sortable\">\n",
        "<thead><tr><th>File</th><th>Coverage%</th><th>Executed Lines</th><th>Total Lines</th>");
      if any_timeexec then
        PrintTo(outstream, "<th>Time</th><th>Statements</th>");
      fi;
      PrintTo(outstream, "</tr></thead>\n");

      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;

      PrintTo(outstream, "</tbody>\n");
      PrintTo(outstream, "</table></body></html>\n");
      CloseStream(outstream);
    end;

    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;

            fileview := rec(outname := outname,
                            inname := infile,
                            execlines := Length(Filtered(fileinfo[2], x -> (x[2] >= 1))));

            if _prof_fileHasTiming(fileinfo) then
                fileview.fileexec := Sum(fileinfo[2], x -> x[2]);
                fileview.filetime := Sum(fileinfo[2], x -> x[3]);
            fi;

            if _prof_fileHasCoverage(fileinfo) then
                fileview.readnotexeclines := Length(Filtered(fileinfo[2], x -> (x[1] >= 1 and x[2] = 0)));
            fi;

            Add(overview, fileview);

            outputhtml(allLines, fileinfo, callinfo[2], calledbyinfo[2], outstream);

            CloseStream(outstream);
        fi;
    od;

    # Just copy the file 'sorttable.js'
    filebuf := ReadAll(InputTextFile(Filename(DirectoriesPackageLibrary( "profiling", "data"), "sorttable.js")));
    outstream := OutputTextFile(Concatenation(outdir, "/sorttable.js"), false);
    SetPrintFormattingStatus(outstream, false);
    PrintTo(outstream, filebuf);
    CloseStream(outstream);


    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;

      outstream := OutputTextFile(Concatenation(outdir, "/funcoverview.html"), false);
      SetPrintFormattingStatus(outstream, false);
      outputfunctablehtml(outstream);
      CloseStream(outstream);

      outputoverviewhtml(overview, outdir, true);
    else
      outputoverviewhtml(overview, outdir, false);
    fi;
end);

# Outputs JSON for consumption by codecov.io
InstallGlobalFunction(OutputJsonCoverage,
function(data, outfile)
    local outstream, lineinfo, prev, file, lines;

    outfile := UserHomeExpand(outfile);
    outstream := IO_File(outfile, "w");

    if not(IsRecord(data)) then
      data := ReadLineByLineProfile(data);
    fi;

    lineinfo := function(lineno, stat)
        if stat[1] > 0 then
            if stat[2] > 0 then
                return STRINGIFY("\"", lineno, "\": \"1\"");
            else
                return STRINGIFY("\"", lineno, "\": \"0\"");
            fi;
        fi;
        return "";
    end;

    IO_Write(outstream, "{ \"coverage\": {\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, Concatenation("\"", file[1], "\": {\n" ));
            lines := List([1..Length(file[2])], n -> lineinfo(n, file[2][n]));
            lines := Filtered(lines, l -> Length(l) > 0);
            IO_Write(outstream, JoinStringsWithSeparator(lines, ",\n"));
            IO_Write(outstream, "}\n");
            prev := true;
        fi;
    od;
    IO_Write(outstream, "} }");
    IO_Close(outstream);
end);

# Outputs JSON for consumption by coveralls
InstallGlobalFunction(OutputCoverallsJsonCoverage,
function(data, outfile, pathtoremove, extraargs...)
    local outstream, lineinfo, prev, file, processfilename,
          lines, opt, env, key;

    if Length(extraargs) > 1 then
        Error("Usage: OutputCoverallsJsonCoverage(data, outfile, pathtoremove[, opt])");
    elif Length(extraargs) = 1 then
        opt := extraargs[1];

        # 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) > 0 then
                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;

    outfile := UserHomeExpand(outfile);
    outstream := IO_File(outfile, "w");

    if not(IsRecord(data)) then
        data := ReadLineByLineProfile(data);
    fi;

    lineinfo := function(lineno, stat)
        if stat[1] > 0 then
            return String(stat[2]);
        else
            return "null";
        fi;
        return "";
    end;

    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\": [");

            lines := List([1..Length(file[2])], n -> lineinfo(n, file[2][n]));
            IO_Write(outstream, JoinStringsWithSeparator(lines, ", "));
            IO_Write(outstream, "]\n}\n");
            prev := true;
        fi;
    od;
    IO_Write(outstream, "] }");
    IO_Close(outstream);
end);

# 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) = 1 then
    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) > 1 then
    ErrorNoReturn("ProfileFile: takes 1 or 2 arguments, but ",
                  Length(args) + 1, " were given");
  fi;

  # Gather data
  rawfile := Filename(opts.outdir, "raw.json");
  if opts.showOutput = true then
    redirect := "";
  else
    redirect := "> /dev/null 2>&1";
  fi;
  len := Length(testfile);
  gap_cmd := GAPInfo.KernelInfo.COMMAND_LINE[1];
  if testfile{[len-3 .. len]} = ".tst" then
    cmd := StringFormatted("""
gapinput="Test(\"{}\"); quit;"
{} --quitonbreak -m 500M -A -q --cover {} {} <<EOF
$gapinput
EOF
    """, testfile, gap_cmd, rawfile, redirect);
  else
    cmd := StringFormatted("""
{} --quitonbreak -m 500M -A -q --cover {} {} {} <<EOF
quit; quit;
EOF
    """, gap_cmd, rawfile, testfile, redirect);
  fi;
  Exec(cmd);

  # 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) = 0 then
    args := [rec()];
  elif Length(args) > 1 then
    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) = 0 then
    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) >= 2 then
    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;

    outfile := UserHomeExpand(outfile);
    outstream := IO_File(outfile, "w");

    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] > 0 then
                IO_Write(outstream, "DA:",i,",",lines[i][2],"\n");
              fi;
            od;
            IO_Write(outstream, "end_of_record\n");
        fi;
    od;
    IO_Close(outstream);
end);

[ Dauer der Verarbeitung: 0.35 Sekunden  (vorverarbeitet)  ]