| e8555c89 Andy McCall |
1 |
#include "Tracer.hh" |
| 2 |
|
| 3 |
#include "Debugger.hh" |
| 4 |
#include "ProbeBreakPoint.hh" |
| 5 |
|
| 6 |
#include "Interpreter.hh" |
| 7 |
#include "MSXMotherBoard.hh" |
| 8 |
#include "ReverseManager.hh" |
| 9 |
#include "StateChangeDistributor.hh" |
| 10 |
#include "TclArgParser.hh" |
| 11 |
|
| 12 |
#include "Date.hh" |
| 13 |
#include "one_of.hh" |
| 14 |
#include "stl.hh" |
| 15 |
#include "strCat.hh" |
| 16 |
|
| 17 |
#include <cassert> |
| 18 |
#include <format> |
| 19 |
#include <queue> |
| 20 |
|
| 21 |
namespace openmsx { |
| 22 |
|
| 23 |
Tracer::Tracer(Debugger& debugger) |
| 24 |
: stateChangeDistributor(debugger.getMotherBoard().getStateChangeDistributor()) |
| 25 |
{ |
| 26 |
stateChangeDistributor.registerListener(*this); |
| 27 |
} |
| 28 |
|
| 29 |
Tracer::~Tracer() |
| 30 |
{ |
| 31 |
stateChangeDistributor.unregisterListener(*this); |
| 32 |
} |
| 33 |
|
| 34 |
void Tracer::Trace::addEvent(EmuTime t, TraceValue v, bool merge) |
| 35 |
{ |
| 36 |
assert(events.empty() || t >= events.back().time); |
| 37 |
|
| 38 |
if (merge && !events.empty() && (events.back().value == v)) { |
| 39 |
return; |
| 40 |
} |
| 41 |
|
| 42 |
using enum Type; |
| 43 |
auto valueFormat = v.visit(overloaded{ |
| 44 |
[](std::monostate) { return MONOSTATE; }, |
| 45 |
[](uint64_t u) { return (u == one_of(0ull, 1ull)) ? BOOL : INTEGER; }, |
| 46 |
[](double) { return DOUBLE; }, |
| 47 |
[](std::string_view) { return STRING; } |
| 48 |
}); |
| 49 |
type = std::max(type, valueFormat); |
| 50 |
events.emplace_back(t, std::move(v)); |
| 51 |
} |
| 52 |
|
| 53 |
void Tracer::Trace::clear() |
| 54 |
{ |
| 55 |
events.clear(); |
| 56 |
} |
| 57 |
|
| 58 |
void Tracer::Trace::attachProbe(Debugger& debugger, ProbeBase& probe) |
| 59 |
{ |
| 60 |
probe.attach(*this); |
| 61 |
motherBoard = &debugger.getMotherBoard(); |
| 62 |
description = probe.getDescription(); |
| 63 |
} |
| 64 |
|
| 65 |
void Tracer::Trace::detachProbe(ProbeBase& probe) |
| 66 |
{ |
| 67 |
probe.detach(*this); |
| 68 |
motherBoard = nullptr; |
| 69 |
description.clear(); |
| 70 |
} |
| 71 |
|
| 72 |
void Tracer::Trace::update(const ProbeBase& subject) noexcept |
| 73 |
{ |
| 74 |
assert(motherBoard); |
| 75 |
if (motherBoard->getReverseManager().isReplaying()) { |
| 76 |
return; |
| 77 |
} |
| 78 |
addEvent(motherBoard->getCurrentTime(), subject.getTraceValue(), false); |
| 79 |
} |
| 80 |
|
| 81 |
void Tracer::probeCreated(Debugger& debugger, ProbeBase& probe) |
| 82 |
{ |
| 83 |
if (auto* trace = findTrace(probe.getName())) { |
| 84 |
trace->attachProbe(debugger, probe); |
| 85 |
} |
| 86 |
} |
| 87 |
|
| 88 |
void Tracer::probeRemoved(ProbeBase& probe) |
| 89 |
{ |
| 90 |
if (auto* trace = findTrace(probe.getName())) { |
| 91 |
trace->detachProbe(probe); |
| 92 |
} |
| 93 |
} |
| 94 |
|
| 95 |
void Tracer::selectProbe(Debugger& debugger, std::string_view name) |
| 96 |
{ |
| 97 |
(void)getOrCreateTrace(debugger, name); |
| 98 |
} |
| 99 |
|
| 100 |
void Tracer::unselectProbe(Debugger& debugger, std::string_view name) |
| 101 |
{ |
| 102 |
dropTrace(debugger, name); |
| 103 |
} |
| 104 |
|
| 105 |
void Tracer::transfer(Debugger& oldDebugger, Debugger& newDebugger) |
| 106 |
{ |
| 107 |
assert(traces.empty()); |
| 108 |
auto& oldTracer = oldDebugger.getTracer(); |
| 109 |
for (auto& trace : oldTracer.traces) { |
| 110 |
if (trace->isProbe()) { |
| 111 |
if (auto* oldProbe = oldDebugger.findProbe(trace->name)) { |
| 112 |
trace->detachProbe(*oldProbe); |
| 113 |
} |
| 114 |
if (auto* newProbe = newDebugger.findProbe(trace->name)) { |
| 115 |
trace->attachProbe(newDebugger, *newProbe); |
| 116 |
} |
| 117 |
} |
| 118 |
traces.push_back(std::move(trace)); |
| 119 |
} |
| 120 |
oldTracer.traces.clear(); |
| 121 |
} |
| 122 |
|
| 123 |
void Tracer::stopReplay(EmuTime time) noexcept |
| 124 |
{ |
| 125 |
|
| 126 |
for (auto& trace : traces) { |
| 127 |
auto it = std::ranges::lower_bound(trace->events, time, {}, &Event::time); |
| 128 |
trace->events.erase(it, trace->events.end()); |
| 129 |
} |
| 130 |
} |
| 131 |
|
| 132 |
Tracer::Trace& Tracer::getOrCreateTrace(Debugger& debugger, std::string_view name) |
| 133 |
{ |
| 134 |
Trace* trace = [&] { |
| 135 |
auto it = std::ranges::lower_bound(traces, name, {}, &Trace::name); |
| 136 |
if (it != traces.end() && (*it)->name == name) { |
| 137 |
return it->get(); |
| 138 |
} else { |
| 139 |
it = traces.insert(it, std::make_unique<Trace>(std::string(name))); |
| 140 |
return it->get(); |
| 141 |
} |
| 142 |
}(); |
| 143 |
if (auto* probe = debugger.findProbe(name)) { |
| 144 |
trace->attachProbe(debugger, *probe); |
| 145 |
} |
| 146 |
return *trace; |
| 147 |
} |
| 148 |
|
| 149 |
Tracer::Trace* Tracer::findTrace(std::string_view name) |
| 150 |
{ |
| 151 |
auto it = std::ranges::lower_bound(traces, name, {}, &Trace::name); |
| 152 |
return (it != traces.end() && (*it)->name == name) ? it->get() : nullptr; |
| 153 |
} |
| 154 |
|
| 155 |
void Tracer::execute(Debugger& debugger, std::span<const TclObject> tokens, TclObject& result, EmuTime time) |
| 156 |
{ |
| 157 |
auto& cmd = debugger.cmd; |
| 158 |
cmd.checkNumArgs(tokens, Completer::AtLeast{3}, "subcommand ?arg ...?"); |
| 159 |
cmd.executeSubCommand(tokens[2].getString(), |
| 160 |
"add", [&]{ add(debugger, tokens, result, time); }, |
| 161 |
"list", [&]{ list(cmd, tokens, result); }, |
| 162 |
"drop", [&]{ drop(debugger, tokens, result); }, |
| 163 |
"probe", [&]{ probe(debugger, tokens, result); }); |
| 164 |
} |
| 165 |
|
| 166 |
void Tracer::add(Debugger& debugger, std::span<const TclObject> tokens, TclObject& , EmuTime time) |
| 167 |
{ |
| 168 |
if (debugger.getMotherBoard().getReverseManager().isReplaying()) { |
| 169 |
return; |
| 170 |
} |
| 171 |
|
| 172 |
std::optional<std::string_view> description; |
| 173 |
std::optional<std::string_view> type; |
| 174 |
std::optional<std::string_view> format; |
| 175 |
bool merge = false; |
| 176 |
std::array info = { |
| 177 |
valueArg("-description", description), |
| 178 |
valueArg("-type", type), |
| 179 |
valueArg("-format", format), |
| 180 |
flagArg("-merge", merge), |
| 181 |
}; |
| 182 |
auto& cmd = debugger.cmd; |
| 183 |
auto& interp = cmd.getInterpreter(); |
| 184 |
auto arguments = parseTclArgs(interp, tokens.subspan(3), info); |
| 185 |
if (arguments.size() != one_of(size_t(1), size_t(2))) { |
| 186 |
interp.wrongNumArgs(3, tokens, "name ?value?"); |
| 187 |
} |
| 188 |
|
| 189 |
Trace& trace = getOrCreateTrace(debugger, arguments[0].getString()); |
| 190 |
if (description) { |
| 191 |
trace.description = *description; |
| 192 |
} |
| 193 |
if (type) { |
| 194 |
using enum Tracer::Trace::Type; |
| 195 |
if (*type == "void") trace.type = MONOSTATE; |
| 196 |
else if (*type == "bool") trace.type = BOOL; |
| 197 |
else if (*type == "int") trace.type = INTEGER; |
| 198 |
else if (*type == "double") trace.type = DOUBLE; |
| 199 |
else if (*type == "string") trace.type = STRING; |
| 200 |
else throw CommandException( |
| 201 |
"Invalid value for -type: ", *type, |
| 202 |
", must be one of void, bool, int, double, string"); |
| 203 |
} |
| 204 |
if (format) { |
| 205 |
using enum Tracer::Trace::Format; |
| 206 |
if (*format == "bin") trace.format = BIN; |
| 207 |
else if (*format == "dec") trace.format = DEC; |
| 208 |
else if (*format == "hex") trace.format = HEX; |
| 209 |
else throw CommandException( |
| 210 |
"Invalid value for -format: ", *format, |
| 211 |
", must be one of bin, dec, hex"); |
| 212 |
} |
| 213 |
|
| 214 |
TraceValue value = [&]{ |
| 215 |
if (arguments.size() < 2) { |
| 216 |
return TraceValue{std::monostate{}}; |
| 217 |
} |
| 218 |
const auto& val = arguments[1]; |
| 219 |
if (auto i = val.getOptionalInt()) { |
| 220 |
return TraceValue{uint64_t(*i)}; |
| 221 |
} else if (auto i64 = val.getOptionalInt64()) { |
| 222 |
return TraceValue{uint64_t(*i64)}; |
| 223 |
} else if (auto d = val.getOptionalDouble()) { |
| 224 |
return TraceValue{*d}; |
| 225 |
} else { |
| 226 |
return TraceValue{val.getString()}; |
| 227 |
} |
| 228 |
}(); |
| 229 |
trace.addEvent(time, std::move(value), merge); |
| 230 |
} |
| 231 |
|
| 232 |
[[nodiscard]] static TclObject toTclObject(const TraceValue& value) |
| 233 |
{ |
| 234 |
return value.visit(overloaded{ |
| 235 |
[](std::monostate) { return TclObject(); }, |
| 236 |
[](uint64_t u) { return TclObject(u); }, |
| 237 |
[](double d) { return TclObject(d); }, |
| 238 |
[](std::string_view s) { return TclObject(s); } |
| 239 |
}); |
| 240 |
} |
| 241 |
|
| 242 |
void Tracer::list(Command& cmd, std::span<const TclObject> tokens, TclObject& result) |
| 243 |
{ |
| 244 |
cmd.checkNumArgs(tokens, Completer::Between{3, 4}, "?name?"); |
| 245 |
if (tokens.size() == 3) { |
| 246 |
|
| 247 |
result.addListElements(std::views::transform(traces, &Trace::name)); |
| 248 |
} else { |
| 249 |
|
| 250 |
std::string_view name = tokens[3].getString(); |
| 251 |
const Trace* trace = findTrace(name); |
| 252 |
if (!trace) { |
| 253 |
throw CommandException("No such trace: ", name); |
| 254 |
} |
| 255 |
for (const auto& event : trace->events) { |
| 256 |
result.addListElement(makeTclList( |
| 257 |
event.time.toDouble(), |
| 258 |
toTclObject(event.value))); |
| 259 |
} |
| 260 |
} |
| 261 |
} |
| 262 |
|
| 263 |
void Tracer::drop(Debugger& debugger, std::span<const TclObject> tokens, TclObject& ) |
| 264 |
{ |
| 265 |
auto& cmd = debugger.cmd; |
| 266 |
cmd.checkNumArgs(tokens, Completer::Between{3, 4}, "?name?"); |
| 267 |
if (tokens.size() == 3) { |
| 268 |
|
| 269 |
traces.clear(); |
| 270 |
} else { |
| 271 |
|
| 272 |
dropTrace(debugger, tokens[3].getString()); |
| 273 |
} |
| 274 |
} |
| 275 |
|
| 276 |
void Tracer::dropTrace(Debugger& debugger, std::string_view name) |
| 277 |
{ |
| 278 |
auto it = std::ranges::lower_bound(traces, name, {}, &Trace::name); |
| 279 |
if (it == traces.end() || (*it)->name != name) { |
| 280 |
|
| 281 |
return; |
| 282 |
} |
| 283 |
if ((*it)->isProbe()) { |
| 284 |
if (auto* probe = debugger.findProbe(name)) { |
| 285 |
(*it)->detachProbe(*probe); |
| 286 |
} |
| 287 |
} |
| 288 |
traces.erase(it); |
| 289 |
} |
| 290 |
|
| 291 |
void Tracer::probe(Debugger& debugger, std::span<const TclObject> tokens, TclObject& ) |
| 292 |
{ |
| 293 |
auto& cmd = debugger.cmd; |
| 294 |
cmd.checkNumArgs(tokens, 4, "name"); |
| 295 |
Trace& trace = getOrCreateTrace(debugger, tokens[3].getString()); |
| 296 |
(void)trace; |
| 297 |
} |
| 298 |
|
| 299 |
void Tracer::tabCompletion(const Debugger& debugger, std::vector<std::string>& tokens) const |
| 300 |
{ |
| 301 |
static constexpr std::array cmds = { |
| 302 |
"add"sv, "list"sv, "drop"sv, "probe"sv, |
| 303 |
}; |
| 304 |
auto& cmd = debugger.cmd; |
| 305 |
if (tokens.size() == 3) { |
| 306 |
cmd.completeString(tokens, cmds); |
| 307 |
} else if (tokens.size() == 4) { |
| 308 |
if (tokens[2] == one_of("add"sv, "list"sv, "drop"sv)) { |
| 309 |
cmd.completeString(tokens, std::views::transform(traces, &Trace::name)); |
| 310 |
} else if (tokens[2] == "probe"sv) { |
| 311 |
cmd.completeString(tokens, std::views::transform(debugger.probes, &ProbeBase::getName)); |
| 312 |
} |
| 313 |
} |
| 314 |
} |
| 315 |
|
| 316 |
[[nodiscard]] std::string Tracer::help(std::span<const TclObject> tokens) const |
| 317 |
{ |
| 318 |
constexpr auto generalHelp = |
| 319 |
"debug trace <subcommand> [<arguments>]\n" |
| 320 |
" Possible subcommands are:\n" |
| 321 |
" add add a new trace data point\n" |
| 322 |
" list list the created traces\n" |
| 323 |
" drop drop trace data\n" |
| 324 |
" probe start collecting probe changes\n" |
| 325 |
" The arguments are specific for each subcommand.\n" |
| 326 |
" Type 'help debug trace <subcommand>' for help about a specific subcommand.\n"; |
| 327 |
|
| 328 |
constexpr auto addHelp = |
| 329 |
"debug trace add <name> [<value>] [-merge] [-description <text>] [-type <t>] [-format <f>]\n" |
| 330 |
" Add a new trace data point to the trace with the given name.\n" |
| 331 |
" If the trace does not exist yet, it will be created.\n" |
| 332 |
" The timestamp of the data point is the current emulation time.\n" |
| 333 |
" The optional value can be an integer, a floating point number,\n" |
| 334 |
" or a string. If no value is given, a void value is used.\n" |
| 335 |
"\n" |
| 336 |
" There are also a few flags that influence the behavior:\n" |
| 337 |
" * -merge: ignore consecutive equal values\n" |
| 338 |
" * -description <text>: gives a description for this trace\n" |
| 339 |
" * -type <t>: must be one of: void, bool, int, double, string\n" |
| 340 |
" E.g. this ensures that a string like '1' is not accidentally interpreted as a bool\n" |
| 341 |
" * -format <f>: must be one of: bin, dec, hex\n" |
| 342 |
" only relevant for type=int\n" |
| 343 |
" The latter 3 options are typically only used once. The first is typically repeated on every invocation."; |
| 344 |
|
| 345 |
constexpr auto listHelp = |
| 346 |
"debug trace list [<name>]\n" |
| 347 |
" Without arguments, list the names of all created traces.\n" |
| 348 |
" With a name argument, list all data points of the given trace.\n" |
| 349 |
" Each data point consists of a timestamp and a value.\n" |
| 350 |
" The timestamp is given as the time (in seconds) since emulation start.\n" |
| 351 |
" Data points are listed in chronological order.\n"; |
| 352 |
|
| 353 |
constexpr auto dropHelp = |
| 354 |
"debug trace drop [<name>]\n" |
| 355 |
" Without arguments, drop all traces and their data points.\n" |
| 356 |
" With a name argument, drop only the trace with the given name.\n"; |
| 357 |
|
| 358 |
constexpr auto probeHelp = |
| 359 |
"debug trace probe <name>\n" |
| 360 |
" Start collecting changes for the given probe. As if on each change there's an automatic 'debug trace add <name> <value>'.\n"; |
| 361 |
|
| 362 |
constexpr auto unknownHelp = |
| 363 |
"Unknown subcommand, use 'help debug trace' to see a list of valid subcommands.\n"; |
| 364 |
|
| 365 |
auto size = tokens.size(); |
| 366 |
assert(size >= 2); |
| 367 |
if (size == 2) { |
| 368 |
return generalHelp; |
| 369 |
} else if (tokens[2] == "add") { |
| 370 |
return addHelp; |
| 371 |
} else if (tokens[2] == "list") { |
| 372 |
return listHelp; |
| 373 |
} else if (tokens[2] == "drop") { |
| 374 |
return dropHelp; |
| 375 |
} else if (tokens[2] == "probe") { |
| 376 |
return probeHelp; |
| 377 |
} else { |
| 378 |
return unknownHelp; |
| 379 |
} |
| 380 |
} |
| 381 |
|
| 382 |
void Tracer::exportVCD(zstring_view filename) |
| 383 |
{ |
| 384 |
|
| 385 |
std::vector<size_t> idx(traces.size(), 0); |
| 386 |
using HeapEntry = std::pair<uint64_t, size_t>; |
| 387 |
std::priority_queue<HeapEntry, std::vector<HeapEntry>, std::greater<HeapEntry>> heap; |
| 388 |
for (size_t i = 0; i < traces.size(); ++i) { |
| 389 |
const auto& t = *traces[i]; |
| 390 |
if (!t.events.empty()) { |
| 391 |
heap.emplace(t.events.front().time.toUint64(), i); |
| 392 |
} |
| 393 |
} |
| 394 |
|
| 395 |
|
| 396 |
std::ofstream os(filename.c_str()); |
| 397 |
if (!os) throw MSXException("Cannot open file for writing: ", filename); |
| 398 |
|
| 399 |
|
| 400 |
os << "$date " << Date::toString(time(nullptr)) << " $end\n" |
| 401 |
"$version openMSX Tracer exportVCD $end\n" |
| 402 |
"$timescale 291005 fs $end\n" |
| 403 |
"$scope module top $end\n"; |
| 404 |
|
| 405 |
|
| 406 |
for (size_t i = 0; i < traces.size(); ++i) { |
| 407 |
const auto& t = *traces[i]; |
| 408 |
os << "$var "; |
| 409 |
switch (t.getType()) { |
| 410 |
using enum Tracer::Trace::Type; |
| 411 |
case MONOSTATE: os << "event 1"; break; |
| 412 |
case BOOL: os << "wire 1"; break; |
| 413 |
case INTEGER: os << "integer 64"; break; |
| 414 |
case DOUBLE: os << "real 64"; break; |
| 415 |
case STRING: os << "string 1"; break; |
| 416 |
} |
| 417 |
os << " t" << i << ' ' << t.name << " $end\n"; |
| 418 |
} |
| 419 |
|
| 420 |
os << "$upscope $end\n" |
| 421 |
"$enddefinitions $end\n"; |
| 422 |
|
| 423 |
std::string tmpBuf; |
| 424 |
auto escapeString = [&](std::string_view str) { |
| 425 |
auto needEscape = [](char c) { return c == '\\' || c <= 32 || c >= 127; }; |
| 426 |
auto it = std::ranges::find_if(str, needEscape); |
| 427 |
if (it == str.end()) return str; |
| 428 |
|
| 429 |
tmpBuf.assign(str.begin(), it); |
| 430 |
do { |
| 431 |
char c = *it; |
| 432 |
if (needEscape(c)) { |
| 433 |
strAppend(tmpBuf, "\\x", hex_string<2>(c)); |
| 434 |
} else { |
| 435 |
tmpBuf += c; |
| 436 |
} |
| 437 |
} while (++it != str.end()); |
| 438 |
return std::string_view{tmpBuf}; |
| 439 |
}; |
| 440 |
|
| 441 |
|
| 442 |
auto emitValue = [&](Tracer::Trace::Type type, size_t id, const TraceValue& val, bool flag) { |
| 443 |
switch (type) { |
| 444 |
using enum Tracer::Trace::Type; |
| 445 |
case MONOSTATE: |
| 446 |
os << (flag ? '1' : '0') << 't' << id << '\n'; |
| 447 |
break; |
| 448 |
case BOOL: { |
| 449 |
uint64_t v = val.get_if<uint64_t>().value_or(0); |
| 450 |
os << (v ? '1' : '0') << 't' << id << '\n'; |
| 451 |
break; |
| 452 |
} |
| 453 |
case INTEGER: { |
| 454 |
uint64_t v = val.get_if<uint64_t>().value_or(0); |
| 455 |
os << 'b' << bin_string(v) << " t" << id << '\n'; |
| 456 |
break; |
| 457 |
} |
| 458 |
case DOUBLE: { |
| 459 |
os << 'r'; |
| 460 |
val.visit(overloaded{ |
| 461 |
[&](std::monostate) { os << '0'; }, |
| 462 |
[&](uint64_t u) { os << u; }, |
| 463 |
[&](double d) { os << std::format("{:.17g}", d); }, |
| 464 |
[&](std::string_view) { os << '0'; } |
| 465 |
}); |
| 466 |
os << " t" << id << '\n'; |
| 467 |
break; |
| 468 |
} |
| 469 |
case STRING: { |
| 470 |
os << "s"; |
| 471 |
val.visit(overloaded{ |
| 472 |
[] (std::monostate) { }, |
| 473 |
[&](uint64_t u) { os << u; }, |
| 474 |
[&](double d) { os << std::format("{:.17g}", d); }, |
| 475 |
[&](std::string_view sv) { os << escapeString(sv); } |
| 476 |
}); |
| 477 |
os << " t" << id << '\n'; |
| 478 |
break; |
| 479 |
} |
| 480 |
} |
| 481 |
}; |
| 482 |
|
| 483 |
|
| 484 |
os << "#0\n" |
| 485 |
"$dumpvars\n"; |
| 486 |
for (size_t i = 0; i < traces.size(); ++i) { |
| 487 |
const auto& t = *traces[i]; |
| 488 |
TraceValue initVal = std::monostate{}; |
| 489 |
if (!t.events.empty()) { |
| 490 |
uint64_t firstNs = t.events.front().time.toUint64(); |
| 491 |
if (firstNs == 0) initVal = t.events.front().value; |
| 492 |
} |
| 493 |
emitValue(t.getType(), i, initVal, false); |
| 494 |
} |
| 495 |
os << "$end\n"; |
| 496 |
|
| 497 |
|
| 498 |
uint64_t curTime = UINT64_MAX; |
| 499 |
while (!heap.empty()) { |
| 500 |
auto [time, tr] = heap.top(); |
| 501 |
heap.pop(); |
| 502 |
if (time != curTime) { |
| 503 |
curTime = time; |
| 504 |
os << '#' << time << '\n'; |
| 505 |
} |
| 506 |
|
| 507 |
size_t pos = idx[tr]; |
| 508 |
const auto& t = *traces[tr]; |
| 509 |
const auto& val = t.events[pos].value; |
| 510 |
emitValue(t.getType(), tr, val, true); |
| 511 |
|
| 512 |
|
| 513 |
++idx[tr]; |
| 514 |
if (idx[tr] < t.events.size()) { |
| 515 |
heap.emplace(t.events[idx[tr]].time.toUint64(), tr); |
| 516 |
} |
| 517 |
} |
| 518 |
} |
| 519 |
|
| 520 |
} |