Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Transaction profiles #547

Merged
merged 39 commits into from
Jan 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
6b200fd
Source locations for transactions and methods
tilk Dec 16, 2023
a5d9e32
More useful source locations
tilk Dec 19, 2023
c658fc7
Source locations in Transactron lib
tilk Dec 19, 2023
edbd3fd
Profiler
tilk Dec 20, 2023
166e4f6
Merge remote-tracking branch 'origin/master' into tilk/transaction-pr…
tilk Dec 20, 2023
b3f160d
Make lint pass
tilk Dec 20, 2023
2bf63f7
Generating profiles
tilk Dec 21, 2023
110a2e9
Shorter ids in profiles
tilk Dec 21, 2023
40cc551
Change one test to get a sensible profile
tilk Dec 21, 2023
8bc4946
Split out profile dataclasses
tilk Dec 21, 2023
c098d20
Use dataclasses_json. Start profiler script
tilk Dec 21, 2023
740948f
Basic profile statistics
tilk Dec 21, 2023
757fcd5
Merge branch 'master' into tilk/transaction-profiles
tilk Dec 21, 2023
dc58151
Start method profiler
tilk Dec 28, 2023
b58e262
Refactor
tilk Dec 29, 2023
91490c0
Towards locked method counter
tilk Dec 31, 2023
3538aee
Some changes
tilk Jan 2, 2024
285fe11
Shorter locations
tilk Jan 2, 2024
873ceae
Unify transaction and method stats
tilk Jan 2, 2024
23431d0
Some documentation
tilk Jan 2, 2024
cc68f4d
Lint
tilk Jan 2, 2024
b0f3688
Recursive statistics
tilk Jan 4, 2024
9ae895a
Recursive locking calculation
tilk Jan 4, 2024
90458a4
Lint
tilk Jan 4, 2024
41be0e2
Make dir, replace Settle with Delay
tilk Jan 8, 2024
a52c43b
Filtering and sorting
tilk Jan 8, 2024
56ab29f
Indented names in call graph
tilk Jan 8, 2024
1052b52
Filtering by source location
tilk Jan 8, 2024
b154c4d
Lint
tilk Jan 8, 2024
db4cad9
Run traces in CI
tilk Jan 8, 2024
dae58df
Merge remote-tracking branch 'origin/master' into tilk/transaction-pr…
tilk Jan 8, 2024
4ed7b2e
Review suggestion
tilk Jan 8, 2024
e0e4a38
Transaction profile
tilk Jan 9, 2024
d914b66
Recursive sorting of trees
tilk Jan 9, 2024
377b2b7
Extend dev env documentation
tilk Jan 10, 2024
753ec78
Clock period dependency in profiler
tilk Jan 12, 2024
52f9a72
Extend doc
tilk Jan 12, 2024
88aaf32
Remove cycles variable
tilk Jan 25, 2024
7c68414
Merge branch 'master' into tilk/transaction-profiles
tilk Jan 26, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -246,8 +246,8 @@ jobs:
- name: Run tests
run: ./scripts/run_tests.py --verbose

- name: Check traces
run: ./scripts/run_tests.py -t -c 1 TestCore
- name: Check traces and profiles
run: ./scripts/run_tests.py -t -p -c 1 TestCore

lint:
name: Check code formatting and typing
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ venv.bak/

# Tests outputs
test/__traces__
test/__profiles__/*.json

# cocotb build
/test/regression/cocotb/build
Expand Down
33 changes: 33 additions & 0 deletions docs/development-environment.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ The `run_tests.py` script has the following options:

* `-l`, `--list` -- lists available tests. This option is helpful, e.g., to find a name of a test generated using the `parameterized` package.
* `-t`, `--trace` -- generates waveforms in the `vcd` format and `gtkw` files for the `gtkwave` tool. The files are saved in the `test/__traces__/` directory. Useful for debugging and test-driven development.
* `-p`, `--profile` -- generates Transactron execution profile information, which can then be read by the script `tprof.py`. The files are saved in the `test/__profile__/` directory. Useful for analyzing performance.
* `-v`, `--verbose` -- makes the test runner more verbose. It will, for example, print the names of all the tests being run.

### lint.sh
Expand Down Expand Up @@ -76,3 +77,35 @@ The `core_graph.py` script has the following options:
### build\_docs.sh

Generates local documentation using [Sphinx](https://www.sphinx-doc.org/). The generated HTML files are located in `build/html`.

### tprof.py

Processes Transactron profile files and presents them in a readable way.
To generate a profile file, the `run_tests.py` script should be used with the `--profile` option.
The `tprof.py` can then be run as follows:

```
scripts/tprof.py test/__profile__/profile_file.json
```

This displays the profile information about transactions by default.
For method profiles, one should use the `--mode=methods` option.

The columns have the following meaning:

* `name` -- the name of the transaction or method in question. The method names are displayed together with the containing module name to differentiate between identically named methods in different modules.
* `source location` -- the file and line where the transaction or method was declared. Used to further disambiguate transaction/methods.
* `locked` -- for methods, shows the number of cycles the method was locked by the caller (called with a false condition). For transactions, shows the number of cycles the transaction could run, but was forced to wait by another, conflicting, transaction.
* `run` -- shows the number of cycles the given method/transaction was running.

To display information about method calls, one can use the `--call-graph` option.
When displaying transaction profiles, this option produces a call graph. For each transaction, there is a tree of methods which are called by this transaction.
Counters presented in the tree shows information about the calls from the transaction in the root of the tree: if a method is also called by a different transaction, these calls are not counted.
When displaying method profiles, an inverted call graph is produced: the transactions are in the leaves, and the children nodes are the callers of the method in question.
In this mode, the `locked` field in the tree shows how many cycles a given method or transaction was responsible for locking the method in the root.

Other options of `tprof.py` are:

* `--sort` -- selects which column is used for sorting rows.
* `--filter-name` -- filters rows by name. Regular expressions can be used.
* `--filter-loc` -- filters rows by source locations. Regular expressions can be used.
2 changes: 2 additions & 0 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,5 @@ cocotb==1.7.2
cocotb-bus==0.2.1
pytest==7.2.2
pyelftools==0.29
dataclasses-json==0.6.3
tabulate==0.9.0
4 changes: 4 additions & 0 deletions scripts/run_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ def main():
parser = argparse.ArgumentParser()
parser.add_argument("-l", "--list", action="store_true", help="List all tests")
parser.add_argument("-t", "--trace", action="store_true", help="Dump waveforms")
parser.add_argument("-p", "--profile", action="store_true", help="Write execution profiles")
parser.add_argument("-v", "--verbose", action="store_true", help="Verbose output")
parser.add_argument("-a", "--all", action="store_true", default=False, help="Run all tests")
parser.add_argument(
Expand All @@ -127,6 +128,9 @@ def main():
if args.trace:
os.environ["__COREBLOCKS_DUMP_TRACES"] = "1"

if args.profile:
os.environ["__TRANSACTRON_PROFILE"] = "1"

if args.test_name:
pattern = re.compile(args.test_name)
unit_tests = {name: test for name, test in unit_tests.items() if pattern.search(name)}
Expand Down
84 changes: 84 additions & 0 deletions scripts/tprof.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
#!/usr/bin/env python3

import argparse
import sys
import re
from pathlib import Path
from typing import Optional
from collections.abc import Callable, Iterable
from tabulate import tabulate
from dataclasses import asdict

topdir = Path(__file__).parent.parent
sys.path.insert(0, str(topdir))


from transactron.profiler import Profile, RunStat, RunStatNode # noqa: E402


def process_stat_tree(
xs: Iterable[RunStatNode], recursive: bool, ret: Optional[list[tuple]] = None, depth=0
) -> list[tuple]:
if ret is None:
ret = list[tuple]()
for x in xs:
row = asdict(x.stat)
if recursive and depth:
row["name"] = (2 * depth - 1) * "-" + " " + row["name"]
ret.append(tuple(row.values()))
if recursive and x.callers:
process_stat_tree(x.callers.values(), recursive, ret, depth + 1)
return ret


def filter_nodes(nodes: list[RunStatNode], key: Callable[[RunStat], str], regex: str):
pattern = re.compile(regex)
return [node for node in nodes if pattern.search(key(node.stat))]


def sort_node(node: RunStatNode, sort_order: str):
node.callers = dict(sorted(node.callers.items(), key=lambda node: asdict(node[1].stat)[sort_order]))
for node2 in node.callers.values():
sort_node(node2, sort_order)


def main():
parser = argparse.ArgumentParser()
parser.add_argument("-g", "--call-graph", action="store_true", help="Show call graph")
parser.add_argument("-s", "--sort", choices=["name", "locked", "run"], default="name", help="Sort by column")
parser.add_argument(
"-m", "--mode", choices=["transactions", "methods"], default="transactions", help="Profile display mode"
)
parser.add_argument("-f", "--filter-name", help="Filter by name, regular expressions can be used")
parser.add_argument("-l", "--filter-loc", help="Filter by source location, regular expressions can be used")
parser.add_argument("file_name", nargs=1)

args = parser.parse_args()

profile = Profile.decode(args.file_name[0])

recursive = args.call_graph

if args.mode == "transactions":
nodes = profile.analyze_transactions(recursive=recursive)
elif args.mode == "methods":
nodes = profile.analyze_methods(recursive=recursive)
else:
assert False

headers = ["name", "source location", "locked", "run"]

nodes.sort(key=lambda node: asdict(node.stat)[args.sort])
for node in nodes:
sort_node(node, args.sort)

if args.filter_name:
nodes = filter_nodes(nodes, lambda stat: stat.name, args.filter_name)
if args.filter_loc:
nodes = filter_nodes(nodes, lambda stat: stat.src_loc, args.filter_loc)

print(tabulate(process_stat_tree(nodes, recursive), headers=headers))


if __name__ == "__main__":
main()
1 change: 1 addition & 0 deletions test/common/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
from .infrastructure import * # noqa: F401
from .sugar import * # noqa: F401
from .testbenchio import * # noqa: F401
from .profiler import * # noqa: F401
from transactron.utils import data_layout # noqa: F401
32 changes: 28 additions & 4 deletions test/common/infrastructure.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from amaranth import *
from amaranth.sim import *
from .testbenchio import TestbenchIO
from .profiler import profiler_process, Profile
from .functions import TestGen
from ..gtkw_extension import write_vcd_ext
from transactron import Method
Expand Down Expand Up @@ -140,12 +141,18 @@ def _wrapping_function(self):


class PysimSimulator(Simulator):
def __init__(self, module: HasElaborate, max_cycles: float = 10e4, add_transaction_module=True, traces_file=None):
def __init__(
self,
module: HasElaborate,
max_cycles: float = 10e4,
add_transaction_module=True,
traces_file=None,
clk_period=1e-6,
):
test_module = _TestModule(module, add_transaction_module)
tested_module = test_module.tested_module
self.tested_module = tested_module = test_module.tested_module
super().__init__(test_module)

clk_period = 1e-6
self.add_clock(clk_period)

if isinstance(tested_module, HasDebugSignals):
Expand Down Expand Up @@ -206,13 +213,30 @@ def run_simulation(self, module: HasElaborate, max_cycles: float = 10e4, add_tra
if "__COREBLOCKS_DUMP_TRACES" in os.environ:
traces_file = unittest.TestCase.id(self)

clk_period = 1e-6
sim = PysimSimulator(
module, max_cycles=max_cycles, add_transaction_module=add_transaction_module, traces_file=traces_file
module,
max_cycles=max_cycles,
add_transaction_module=add_transaction_module,
traces_file=traces_file,
clk_period=clk_period,
)
self.add_all_mocks(sim, sys._getframe(2).f_locals)
yield sim

profile = None
if "__TRANSACTRON_PROFILE" in os.environ and isinstance(sim.tested_module, TransactionModule):
profile = Profile()
sim.add_sync_process(profiler_process(sim.tested_module.transactionManager, profile, clk_period))

res = sim.run()

if profile is not None:
profile_dir = "test/__profiles__"
profile_file = unittest.TestCase.id(self)
os.makedirs(profile_dir, exist_ok=True)
profile.encode(f"{profile_dir}/{profile_file}.json")

self.assertTrue(res, "Simulation time limit exceeded")

def tick(self, cycle_cnt: int = 1):
Expand Down
85 changes: 85 additions & 0 deletions test/common/profiler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import os.path
from amaranth.sim import *
from transactron.core import MethodMap, TransactionManager
from transactron.profiler import CycleProfile, Profile, ProfileInfo
from transactron.utils import SrcLoc
from .functions import TestGen

__all__ = ["profiler_process"]


def profiler_process(transaction_manager: TransactionManager, profile: Profile, clk_period: float):
def process() -> TestGen:
method_map = MethodMap(transaction_manager.transactions)
cgr, _, _ = TransactionManager._conflict_graph(method_map)
id_map = dict[int, int]()
id_seq = 0

def get_id(obj):
try:
return id_map[id(obj)]
except KeyError:
nonlocal id_seq
id_seq = id_seq + 1
id_map[id(obj)] = id_seq
return id_seq

def local_src_loc(src_loc: SrcLoc):
return (os.path.relpath(src_loc[0]), src_loc[1])

for transaction in method_map.transactions:
profile.transactions_and_methods[get_id(transaction)] = ProfileInfo(
transaction.owned_name, local_src_loc(transaction.src_loc), True
)

for method in method_map.methods:
profile.transactions_and_methods[get_id(method)] = ProfileInfo(
method.owned_name, local_src_loc(method.src_loc), False
)

yield Passive()
while True:
yield Delay((1 - 1e-4) * clk_period) # shorter than one clock cycle

cprof = CycleProfile()
profile.cycles.append(cprof)

for transaction in method_map.transactions:
request = yield transaction.request
runnable = yield transaction.runnable
grant = yield transaction.grant

if grant:
cprof.running[get_id(transaction)] = None
elif request and runnable:
for transaction2 in cgr[transaction]:
if (yield transaction2.grant):
cprof.locked[get_id(transaction)] = get_id(transaction2)

running = set(cprof.running)
for method in method_map.methods:
if (yield method.run):
running.add(get_id(method))

locked_methods = set[int]()
for method in method_map.methods:
if get_id(method) not in running:
if any(get_id(transaction) in running for transaction in method_map.transactions_by_method[method]):
locked_methods.add(get_id(method))

for method in method_map.methods:
if get_id(method) in running:
for t_or_m in method_map.method_parents[method]:
if get_id(t_or_m) in running:
cprof.running[get_id(method)] = get_id(t_or_m)
elif get_id(method) in locked_methods:
caller = next(
get_id(t_or_m)
for t_or_m in method_map.method_parents[method]
if get_id(t_or_m) in running or get_id(t_or_m) in locked_methods
)
cprof.locked[get_id(method)] = caller

yield

return process
5 changes: 3 additions & 2 deletions test/transactions/test_methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -612,7 +612,6 @@ def process():
yield self.circ.req_t2.eq(req_t2)
yield self.circ.ready.eq(m_ready)
yield Settle()
yield Delay(1e-8)

out_m = yield self.circ.out_m
out_t1 = yield self.circ.out_t1
Expand All @@ -632,8 +631,10 @@ def process():
self.assertTrue(in1 != self.bad_number or not out_t1)
self.assertTrue(in2 != self.bad_number or not out_t2)

yield

with self.run_simulation(self.circ, 100) as sim:
sim.add_process(process)
sim.add_sync_process(process)
lekcyjna123 marked this conversation as resolved.
Show resolved Hide resolved

def test_random_arg(self):
self.base_random(lambda arg: arg.data != self.bad_number)
Expand Down
5 changes: 5 additions & 0 deletions transactron/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ def __init__(self, transactions: Iterable["Transaction"]):
self.methods_by_transaction = dict[Transaction, list[Method]]()
self.transactions_by_method = defaultdict[Method, list[Transaction]](list)
self.readiness_by_method_and_transaction = dict[tuple[Transaction, Method], ValueLike]()
self.method_parents = defaultdict[Method, list[TransactionBase]](list)

def rec(transaction: Transaction, source: TransactionBase):
for method, (arg_rec, _) in source.method_uses.items():
Expand All @@ -92,6 +93,10 @@ def rec(transaction: Transaction, source: TransactionBase):
self.methods_by_transaction[transaction] = []
rec(transaction, transaction)

for transaction_or_method in self.methods_and_transactions:
for method in transaction_or_method.method_uses.keys():
self.method_parents[method].append(transaction_or_method)

def transactions_for(self, elem: TransactionOrMethod) -> Collection["Transaction"]:
if isinstance(elem, Transaction):
return [elem]
Expand Down
Loading
Loading