diff --git a/README.md b/README.md index 8dba794..4b20d55 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ Verify, optimize, and explore cycle-approximate simulation with uScope! ```bash pip install --upgrade pip setuptools wheel -git git@github.com:ProteusLab/uScope.git +git clone git@github.com:ProteusLab/uScope.git cd uScope pip install -e . ``` diff --git a/examples/reference/reference.json.gz b/examples/reference/reference.json.gz index 188747d..83b6b6b 100644 Binary files a/examples/reference/reference.json.gz and b/examples/reference/reference.json.gz differ diff --git a/pyproject.toml b/pyproject.toml index 9d82a7f..781fd98 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,6 +14,7 @@ classifiers = [ "License :: OSI Approved :: Apache 2.0 License", ] requires-python = ">=3.8" +dependencies = ["tqdm>=4.62"] [project.scripts] uScope = "uScope.main:main" diff --git a/src/uScope/O3.py b/src/uScope/O3.py index 269d3b7..2297f63 100644 --- a/src/uScope/O3.py +++ b/src/uScope/O3.py @@ -10,6 +10,7 @@ class PipelineStage(Enum): ISSUE = "issue" COMPLETE = "complete" RETIRE = "retire" + STORE_COMPLETE = "store_complete" @staticmethod def order(): @@ -34,6 +35,7 @@ class Instruction: opclass: str stages: Dict[PipelineStage, int] stage_order: List[PipelineStage] + store_tick: int = 0 @property def mnemonic(self): @@ -41,6 +43,10 @@ def mnemonic(self): return Instruction.UNKNOWN return self.disasm.split()[0].upper() + @property + def is_squashed(self) -> bool: + return self.stages.get(PipelineStage.RETIRE, 0) == 0 + # NOTE: https://github.com/gem5/gem5/blob/stable/src/cpu/FuncUnit.py class OpClass(Enum): IntAlu = "IntAlu" diff --git a/src/uScope/config.py b/src/uScope/config.py index ebc5aed..d002e4c 100644 --- a/src/uScope/config.py +++ b/src/uScope/config.py @@ -1,4 +1,5 @@ import json +import logging from pathlib import Path from typing import Any, Optional from abc import ABC, abstractmethod @@ -6,6 +7,9 @@ from .O3 import PipelineStage, OpClass, Instruction from .utils import stable_hash +logger = logging.getLogger(__name__) + + class IConfig(ABC): @abstractmethod def get_func_unit(self, opclass: OpClass) -> str: @@ -16,9 +20,17 @@ def get_stage_name(self, stage : PipelineStage) -> str: assert False, f"Pipeline stage name mapping method wasn't defined in {type(self).__name__}" @abstractmethod - def get_color_for_instr(self, instr : Instruction) -> str: + def get_color_for_instr(self, instr: Instruction) -> str: assert False, f"Color mapping method wasn't defined in {type(self).__name__}" + @abstractmethod + def get_squashed_cname(self) -> str: + assert False, f"Squashed color method wasn't defined in {type(self).__name__}" + + @abstractmethod + def store_completions_pid(self) -> int: + assert False, f"Store Completions Process ID wasn't defined in {type(self).__name__}" + @abstractmethod def pipeline_pid(self) -> int: assert False, f"Pipeline Process ID wasn't defined in {type(self).__name__}" @@ -35,14 +47,21 @@ def pipeline_width(self) -> int: def func_units_width(self) -> int: assert False, f"Functional Units width wasn't defined in {type(self).__name__}" + class Config(IConfig): def __init__(self, data: dict): self._data = data - for key, value in data.items(): + + def __getattr__(self, name: str): + key = name.lstrip("_") + if key in self._data: + value = self._data[key] if isinstance(value, dict): - setattr(self, f"_{key}", Config(value)) - else: - setattr(self, f"_{key}", value) + return Config(value) + return value + raise AttributeError( + f"'{type(self).__name__}' object has no attribute '{name}'" + ) @property def settings(self): @@ -64,21 +83,28 @@ def pipeline_width(self) -> int: def func_units_width(self) -> int: return self.settings._MAX_FUNC_UNITS_WIDTH + @property + def store_completions_pid(self) -> int: + return self.settings._PID_STORE_COMPLETIONS_BASE + def get_func_unit(self, opclass: Any) -> str: return self._func_units.get(str(opclass), "No_OpClass") - def get_stage_name(self, stage : PipelineStage) -> str: + def get_stage_name(self, stage: PipelineStage) -> str: return self._stage_names[stage.value] def get_color_for_func_unit(self, unit) -> str: return self._colors.get(unit, self._colors._default) - def get_color_for_instr(self, instr : Instruction) -> str: + def get_color_for_instr(self, instr: Instruction) -> str: unit = self.get_func_unit(instr.opclass) family = self._colors.get(unit, self._colors._default) idx = stable_hash(instr.mnemonic, len(family)) return family[idx] + def get_squashed_cname(self) -> str: + return self._colors._squashed + def __getitem__(self, key: str) -> Any: return self._data[key] @@ -100,13 +126,18 @@ def load_config(config_path: Optional[Path] = None) -> Config: if builtin_dir.is_dir(): for json_file in builtin_dir.glob("*.json"): key = json_file.stem - with open(json_file, 'r', encoding='utf-8') as f: + with open(json_file, "r", encoding="utf-8") as f: config_data[key] = json.load(f) - if config_path is not None and config_path.is_dir(): - for json_file in config_path.glob("*.json"): - key = json_file.stem - with open(json_file, 'r', encoding='utf-8') as f: - config_data[key] = json.load(f) + if config_path is not None: + if config_path.is_dir(): + for json_file in config_path.glob("*.json"): + key = json_file.stem + with open(json_file, "r", encoding="utf-8") as f: + config_data[key] = json.load(f) + else: + logger.warning( + "Config path '%s' is not a directory — ignoring", config_path + ) return Config(config_data) diff --git a/src/uScope/configs/colors.json b/src/uScope/configs/colors.json index 15d554d..58315c0 100644 --- a/src/uScope/configs/colors.json +++ b/src/uScope/configs/colors.json @@ -80,5 +80,6 @@ "good", "rail_idle", "yellow" - ] + ], + "squashed": "grey" } diff --git a/src/uScope/configs/settings.json b/src/uScope/configs/settings.json index 0d944eb..2e48e81 100644 --- a/src/uScope/configs/settings.json +++ b/src/uScope/configs/settings.json @@ -1,6 +1,7 @@ { "PID_PIPELINE_STAGES_BASE": 100, "PID_FUNC_UNITS_BASE": 200, + "PID_STORE_COMPLETIONS_BASE": 300, "MAX_PIPE_WIDTH": 256, "MAX_FUNC_UNITS_WIDTH": 8 } diff --git a/src/uScope/configs/stage_names.json b/src/uScope/configs/stage_names.json index 9624bdf..9417f49 100644 --- a/src/uScope/configs/stage_names.json +++ b/src/uScope/configs/stage_names.json @@ -5,5 +5,6 @@ "dispatch": "Dispatch", "issue": "Issue", "complete": "Complete", - "retire": "Retire" + "retire": "Retire", + "store_complete": "Store Complete" } diff --git a/src/uScope/converter.py b/src/uScope/converter.py index 07ced6c..033982f 100644 --- a/src/uScope/converter.py +++ b/src/uScope/converter.py @@ -1,34 +1,62 @@ -from typing import Dict, List, Tuple, Any +from typing import Dict, List, Tuple + +from tqdm import tqdm from .O3 import PipelineStage, Instruction from .events import MetadataEvent, DurationEvent -from .thread_pool import ThreadPoolManager +from .thread_pool import StageLaneManager from .config import IConfig from .parser import PipeViewParser + class ChromeTracingConverter: - def __init__(self, parser : PipeViewParser, config : IConfig, exclude_exec : bool = False, exclude_pipeline : bool = False): - self.parser : PipeViewParser = parser + def __init__( + self, + parser: PipeViewParser, + config: IConfig, + exclude_exec: bool = False, + exclude_pipeline: bool = False, + only_committed: bool = False, + store_completions: bool = True, + ): + self.parser: PipeViewParser = parser if not isinstance(config, IConfig): - raise TypeError(f"Unexpetcted Config type {type(config).__name__}. Please, derive you configuration class from {IConfig}") - self.config : IConfig = config + raise TypeError( + f"Unexpected Config type {type(config).__name__}. " + f"Please derive your configuration class from {IConfig}" + ) + self.config: IConfig = config - self.exclude_exec : bool = exclude_exec - self.exclude_pipeline : bool = exclude_pipeline + self.exclude_exec: bool = exclude_exec + self.exclude_pipeline: bool = exclude_pipeline + self.only_committed: bool = only_committed + self.store_completions: bool = store_completions self.metadata_events: List[MetadataEvent] = [] self.duration_events: List[DurationEvent] = [] - self.stage_managers: Dict[PipelineStage, ThreadPoolManager] = {} - self.func_units_managers: Dict[str, ThreadPoolManager] = {} + self.stage_managers: Dict[PipelineStage, StageLaneManager] = {} + self.func_units_managers: Dict[str, StageLaneManager] = {} + self.store_lane_manager: StageLaneManager = None - def convert(self) -> List[dict]: + def convert(self, progress: bool = True) -> List[dict]: self._add_metadata() - for instr in self.instructions_by_seq_num(): + instructions = self.instructions_by_seq_num() + for instr in tqdm( + instructions, + desc="Converting", + unit="instr", + disable=not progress, + leave=False, + ): + if self.only_committed and instr.is_squashed: + continue if not self.exclude_pipeline: self._add_pipeline_stage_events(instr) if not self.exclude_exec: self._add_execution_unit_events(instr) + if self.store_completions and instr.store_tick > 0: + self._add_store_completion_event(instr) return [e.to_dict() for e in self.metadata_events + self.duration_events] @@ -40,6 +68,8 @@ def _add_metadata(self): self._add_pipeline_stages_metadata() if not self.exclude_exec: self._add_execution_units_metadata() + if self.store_completions: + self._add_store_completions_metadata() def _add_pipeline_stages_metadata(self): for id, stage in enumerate(PipelineStage.order()): @@ -47,30 +77,18 @@ def _add_pipeline_stages_metadata(self): process_name = f"{(id + 1):02d}_{stage_name}" pid = self.config.pipeline_pid + id - manager = ThreadPoolManager( + manager = StageLaneManager( max_width=self.config.pipeline_width, pid=pid, - thread_name_prefix=stage_name, - metadata_events=self.metadata_events + lane_name_prefix=stage_name, + metadata_events=self.metadata_events, ) self.stage_managers[stage] = manager - manager.add_initial_thread(0) - self.metadata_events.append(MetadataEvent( - name="process_name", pid=pid, - args={"name": process_name} - )) - - self.metadata_events.append(MetadataEvent( - name="thread_name", pid=pid, tid=0, - args={"name": f"00_{stage_name}"} - )) - - self.metadata_events.append(MetadataEvent( - name="thread_sort_index", pid=pid, tid=0, - args={"sort_index": 0} - )) + self.metadata_events.append( + MetadataEvent(name="process_name", pid=pid, args={"name": process_name}) + ) def _add_execution_units_metadata(self): unit_names = set() @@ -81,41 +99,43 @@ def _add_execution_units_metadata(self): for i, unit_name in enumerate(sorted(unit_names)): pid = self.config.func_units_pid + i - manager = ThreadPoolManager( + manager = StageLaneManager( max_width=self.config.func_units_width, pid=pid, - thread_name_prefix=unit_name, - metadata_events=self.metadata_events + lane_name_prefix=unit_name, + metadata_events=self.metadata_events, ) self.func_units_managers[unit_name] = manager - manager.add_initial_thread(0) - self.metadata_events.append(MetadataEvent( - name="process_name", pid=pid, - args={"name": f"{unit_name}"} - )) - - self.metadata_events.append(MetadataEvent( - name="thread_name", pid=pid, tid=0, - args={"name": f"00_{unit_name}"} - )) - - self.metadata_events.append(MetadataEvent( - name="thread_sort_index", pid=pid, tid=0, - args={"sort_index": 0} - )) + self.metadata_events.append( + MetadataEvent( + name="process_name", pid=pid, args={"name": f"{unit_name}"} + ) + ) - def _assign_thread_for_stage(self, stage: PipelineStage, start_time: int, end_time: int) -> Tuple[int, int]: - return self.stage_managers[stage].assign_thread(start_time, end_time) + def _assign_lane_for_stage( + self, stage: PipelineStage, start_time: int, end_time: int + ) -> Tuple[int, int]: + return self.stage_managers[stage].assign_lane(start_time, end_time) - def _assign_thread_for_func_units(self, unit_name: str, start_time: int, end_time: int) -> Tuple[int, int]: - return self.func_units_managers[unit_name].assign_thread(start_time, end_time) + def _assign_lane_for_func_units( + self, unit_name: str, start_time: int, end_time: int + ) -> Tuple[int, int]: + return self.func_units_managers[unit_name].assign_lane(start_time, end_time) - def _add_pipeline_stage_events(self, instr : Instruction): + def _add_pipeline_stage_events(self, instr: Instruction): mnemonic = instr.mnemonic - cname = self.config.get_color_for_instr(instr) - - active = [(st, instr.stages[st]) for st in instr.stage_order if instr.stages.get(st, 0) > 0] + cname = ( + self.config.get_squashed_cname() + if instr.is_squashed + else self.config.get_color_for_instr(instr) + ) + + active = [ + (st, instr.stages[st]) + for st in instr.stage_order + if instr.stages.get(st, 0) > 0 + ] if not active: return active.sort(key=lambda x: x[1]) @@ -123,26 +143,28 @@ def _add_pipeline_stage_events(self, instr : Instruction): for i, (stage, tick) in enumerate(active): dur = max(1, active[i + 1][1] - tick) if i < len(active) - 1 else 1 start, end = tick, tick + dur - pid, tid = self._assign_thread_for_stage(stage, start, end) - - self.duration_events.append(DurationEvent( - name=mnemonic, - cat=self.config.get_stage_name(stage), - ts=tick, - dur=dur, - pid=pid, - tid=tid, - cname=cname, - args={ - "PC": instr.pc, - "SeqNum": instr.seq_num, - "Stage": self.config.get_stage_name(stage), - "OpClass": instr.opclass, - "Disasm": instr.disasm - } - )) + pid, tid = self._assign_lane_for_stage(stage, start, end) + + self.duration_events.append( + DurationEvent( + name=mnemonic, + cat=self.config.get_stage_name(stage), + ts=tick, + dur=dur, + pid=pid, + tid=tid, + cname=cname, + args={ + "PC": instr.pc, + "SeqNum": instr.seq_num, + "Stage": self.config.get_stage_name(stage), + "OpClass": instr.opclass, + "Disasm": instr.disasm, + }, + ) + ) - def _add_execution_unit_events(self, instr : Instruction): + def _add_execution_unit_events(self, instr: Instruction): if not instr.opclass: return @@ -157,23 +179,91 @@ def _add_execution_unit_events(self, instr : Instruction): if unit not in self.func_units_managers: return - pid, tid = self._assign_thread_for_func_units(unit, issue, complete) + pid, tid = self._assign_lane_for_func_units(unit, issue, complete) dur = complete - issue + cname = ( + self.config.get_squashed_cname() + if instr.is_squashed + else self.config.get_color_for_instr(instr) + ) + + self.duration_events.append( + DurationEvent( + name=mnemonic, + cat=unit, + ts=issue, + dur=dur, + pid=pid, + tid=tid, + cname=cname, + args={ + "PC": instr.pc, + "SeqNum": instr.seq_num, + "OpClass": instr.opclass, + "Unit": unit, + "Duration": dur, + "Disasm": instr.disasm, + }, + ) + ) + + def _add_store_completions_metadata(self): + store_name = self.config.get_stage_name(PipelineStage.STORE_COMPLETE) + pid = self.config.store_completions_pid - self.duration_events.append(DurationEvent( - name=mnemonic, - cat=unit, - ts=issue, - dur=dur, + manager = StageLaneManager( + max_width=self.config.pipeline_width, pid=pid, - tid=tid, - cname=self.config.get_color_for_instr(instr), - args={ - "PC": instr.pc, - "SeqNum": instr.seq_num, - "OpClass": instr.opclass, - "Unit": unit, - "Duration": dur, - "Disasm": instr.disasm - } - )) + lane_name_prefix=store_name, + metadata_events=self.metadata_events, + ) + self.store_lane_manager = manager + + self.metadata_events.append( + MetadataEvent( + name="process_name", + pid=pid, + args={"name": store_name}, + ) + ) + + def _add_store_completion_event(self, instr: Instruction): + retire_tick = instr.stages.get(PipelineStage.RETIRE, 0) + store_tick = instr.store_tick + if retire_tick <= 0 or store_tick <= 0 or store_tick <= retire_tick: + return + + mnemonic = instr.mnemonic + store_name = self.config.get_stage_name(PipelineStage.STORE_COMPLETE) + + pid = self.config.store_completions_pid + tid = 0 + if self.store_lane_manager is not None: + _, tid = self.store_lane_manager.assign_lane(retire_tick, store_tick) + + dur = store_tick - retire_tick + cname = ( + self.config.get_squashed_cname() + if instr.is_squashed + else self.config.get_color_for_instr(instr) + ) + + self.duration_events.append( + DurationEvent( + name=mnemonic, + cat=store_name, + ts=retire_tick, + dur=dur, + pid=pid, + tid=tid, + cname=cname, + args={ + "PC": instr.pc, + "SeqNum": instr.seq_num, + "OpClass": instr.opclass, + "Stage": store_name, + "Duration": dur, + "Disasm": instr.disasm, + }, + ) + ) diff --git a/src/uScope/main.py b/src/uScope/main.py index e60d7e8..a3776fd 100644 --- a/src/uScope/main.py +++ b/src/uScope/main.py @@ -8,6 +8,7 @@ from pathlib import Path +from . import __version__ from .parser import PipeViewParser from .converter import ChromeTracingConverter from .config import load_config @@ -52,9 +53,51 @@ def main(): action="store_true", help="Compress output JSON with gzip" ) + parser.add_argument( + "--version", "-V", + action="version", + version=f"uScope {__version__}", + help="Show version and exit" + ) + parser.add_argument( + "--verbose", "-v", + default=False, + action="store_true", + help="Enable verbose (DEBUG level) output" + ) + parser.add_argument( + "--quiet", "-q", + default=False, + action="store_true", + help="Suppress progress bar and reduce log output" + ) + parser.add_argument( + "--only-committed", + default=False, + action="store_true", + help="Exclude squashed (incomplete) instructions from output" + ) + parser.add_argument( + "--store-completions", + default=True, + action="store_true", + help="Include store completion tick events (enabled by default)" + ) + parser.add_argument( + "--no-store-completions", + dest="store_completions", + action="store_false", + help="Disable store completion tick events" + ) + args = parser.parse_args() + if args.verbose: + logging.getLogger().setLevel(logging.DEBUG) + elif args.quiet: + logging.getLogger().setLevel(logging.WARNING) + input_file = args.input_file if args.output_file: @@ -72,26 +115,26 @@ def main(): if not Path(input_file).exists(): raise FileNotFoundError(f"Input file not found: {input_file}") - logging.warning(f"Parsing {input_file}") + logger.info(f"Parsing {input_file}") trace_parser = PipeViewParser() trace_parser.parse_file(input_file) if not trace_parser.instructions: raise ValueError("No instructions with valid timestamps found") - logging.warning(f"Loading configuration from {args.config_path if args.config_path else 'default location'}") + logger.info(f"Loading configuration from {args.config_path if args.config_path else 'default location'}") config = load_config(args.config_path) - converter = ChromeTracingConverter(trace_parser, config, args.exclude_exec, args.exclude_pipeline) - events = converter.convert() + converter = ChromeTracingConverter(trace_parser, config, args.exclude_exec, args.exclude_pipeline, args.only_committed, args.store_completions) + events = converter.convert(progress=not args.quiet) - logging.warning(f"Loading {output_file}") + logger.info(f"Writing {output_file}") with (gzip.open if args.gzip else open)(output_file, 'wt', encoding='utf-8') as f: json.dump(events, f, indent=2) - logging.warning(f"Total events: {len(events)}") + logger.info(f"Total events: {len(events)}") except ValueError as e: logging.error(f"Value error: {e}") diff --git a/src/uScope/parser.py b/src/uScope/parser.py index e90a927..dd128fa 100644 --- a/src/uScope/parser.py +++ b/src/uScope/parser.py @@ -1,23 +1,17 @@ -import re - from .O3 import Instruction, PipelineStage + class PipeViewParser: - FETCH_PATTERN = re.compile( - r'O3PipeView:fetch:(\d+):(0x[0-9a-f]+):\d+:(\d+):([^:]+):(.+)$' - ) - STAGE_PATTERN = re.compile( - r'O3PipeView:(\w+):(\d+)(?::.*)?$' - ) + PREFIX = "O3PipeView:" def __init__(self): self.instructions = {} self.current_seq_num = None self.current_instr = None - self.stage_map = { f"{stage}" : stage for stage in PipelineStage.order() } + self.stage_map = {f"{stage}": stage for stage in PipelineStage.order()} def parse_file(self, filename: str): - with open(filename, 'r') as f: + with open(filename, "r") as f: for line in f: line = line.strip() if line: @@ -27,22 +21,52 @@ def parse_file(self, filename: str): self.instructions[self.current_seq_num] = self.current_instr self.instructions = { - seq: instr for seq, instr in self.instructions.items() - if sum(1 for tick in instr.stages.values() if tick > 0) > 0 + seq: instr + for seq, instr in self.instructions.items() + if any(tick > 0 for tick in instr.stages.values()) } + @staticmethod + def _parse_fetch_line(rest: str): + parts = rest.split(":", 5) + if len(parts) != 6: + return None + tick_str, pc, _, seq_str, disasm, opclass = parts + return int(tick_str), pc, int(seq_str), disasm.strip(), opclass.strip() + + @staticmethod + def _parse_stage_line(rest: str): + idx = rest.index(":") + stage_name = rest[:idx] + rest = rest[idx + 1 :] + idx = rest.find(":") + if idx == -1: + tick = int(rest) + store_tick = 0 + else: + tick = int(rest[:idx]) + remaining = rest[idx + 1 :] + if remaining.startswith("store:"): + store_tick = int(remaining[6:]) + else: + store_tick = 0 + return stage_name, tick, store_tick + def parse_line(self, line: str): - fetch_match = self.FETCH_PATTERN.match(line) - if fetch_match: + if not line.startswith(self.PREFIX): + return + + rest = line[len(self.PREFIX) :] + + if rest.startswith("fetch:"): + result = self._parse_fetch_line(rest[6:]) + if result is None: + return + tick, pc, seq_num, disasm, opclass = result + if self.current_instr is not None: self.instructions[self.current_seq_num] = self.current_instr - tick = int(fetch_match.group(1)) - pc = fetch_match.group(2) - seq_num = int(fetch_match.group(3)) - disasm = fetch_match.group(4).strip() - opclass = fetch_match.group(5).strip() - self.current_seq_num = seq_num self.current_instr = Instruction( seq_num=seq_num, @@ -50,17 +74,16 @@ def parse_line(self, line: str): disasm=disasm, opclass=opclass, stages={}, - stage_order=[] + stage_order=[], ) self.current_instr.stages[PipelineStage.FETCH] = tick self.current_instr.stage_order.append(PipelineStage.FETCH) return - stage_match = self.STAGE_PATTERN.match(line) - if stage_match and self.current_instr is not None: - stage_name = stage_match.group(1).lower() - tick = int(stage_match.group(2)) + if self.current_instr is not None: + stage_name, tick, store_tick = self._parse_stage_line(rest) + stage_name = stage_name.lower() if stage_name in self.stage_map: stage = self.stage_map[stage_name] @@ -68,3 +91,6 @@ def parse_line(self, line: str): if stage not in self.current_instr.stage_order: self.current_instr.stage_order.append(stage) + + if stage_name == PipelineStage.RETIRE.value and store_tick > 0: + self.current_instr.store_tick = store_tick diff --git a/src/uScope/thread_pool.py b/src/uScope/thread_pool.py index 1067ccc..63f2810 100644 --- a/src/uScope/thread_pool.py +++ b/src/uScope/thread_pool.py @@ -2,22 +2,28 @@ from .events import MetadataEvent -class ThreadPoolManager: - def __init__(self, max_width: int, pid: int, thread_name_prefix: str, + +class StageLaneManager: + def __init__(self, max_width: int, pid: int, lane_name_prefix: str, metadata_events: List[MetadataEvent]): self.max_width = max_width self.pid = pid - self.thread_name_prefix = thread_name_prefix + self.lane_name_prefix = lane_name_prefix self.metadata_events = metadata_events - self.pool: List[Tuple[int, int]] = [] - self.next_tid = 0 - - def add_initial_thread(self, end_time: int = 0): - self.pool.append((end_time, 0)) + self.pool: List[Tuple[int, int]] = [(0, 0)] self.next_tid = 1 - def assign_thread(self, start_time: int, end_time: int) -> Tuple[int, int]: + self.metadata_events.append(MetadataEvent( + name="thread_name", pid=self.pid, tid=0, + args={"name": f"00_{self.lane_name_prefix}"} + )) + self.metadata_events.append(MetadataEvent( + name="thread_sort_index", pid=self.pid, tid=0, + args={"sort_index": 0} + )) + + def assign_lane(self, start_time: int, end_time: int) -> Tuple[int, int]: for i, (last_end, tid) in enumerate(self.pool): if last_end <= start_time: self.pool[i] = (end_time, tid) @@ -30,7 +36,7 @@ def assign_thread(self, start_time: int, end_time: int) -> Tuple[int, int]: self.metadata_events.append(MetadataEvent( name="thread_name", pid=self.pid, tid=new_tid, - args={"name": f"{new_tid:02d}_{self.thread_name_prefix}"} + args={"name": f"{new_tid:02d}_{self.lane_name_prefix}"} )) self.metadata_events.append(MetadataEvent( name="thread_sort_index", pid=self.pid, tid=new_tid, diff --git a/tests/test_thread_pool.py b/tests/test_thread_pool.py index 7e645b4..ac36155 100644 --- a/tests/test_thread_pool.py +++ b/tests/test_thread_pool.py @@ -1,48 +1,47 @@ import pytest -from uScope.thread_pool import ThreadPoolManager +from uScope.thread_pool import StageLaneManager -def test_reuse_existing_thread(): +def test_reuse_existing_lane(): metadata = [] - manager = ThreadPoolManager( - max_width=2, pid=100, thread_name_prefix="Test", metadata_events=metadata + manager = StageLaneManager( + max_width=2, pid=100, lane_name_prefix="Test", metadata_events=metadata ) - manager.add_initial_thread(end_time=10) - pid, tid = manager.assign_thread(start_time=15, end_time=20) + pid, tid = manager.assign_lane(start_time=15, end_time=20) assert pid == 100 and tid == 0 assert manager.pool[0][0] == 20 - assert len(metadata) == 0 -def test_assign_new_thread(): +def test_assign_new_lane(): metadata = [] - manager = ThreadPoolManager( - max_width=2, pid=100, thread_name_prefix="Test", metadata_events=metadata + manager = StageLaneManager( + max_width=2, pid=100, lane_name_prefix="Test", metadata_events=metadata ) - manager.add_initial_thread(end_time=10) - pid, tid = manager.assign_thread(start_time=5, end_time=15) + manager.assign_lane(start_time=0, end_time=10) + + pid, tid = manager.assign_lane(start_time=5, end_time=15) assert pid == 100 and tid == 1 assert len(manager.pool) == 2 - assert len(metadata) == 2 - assert metadata[0].tid == 1 and metadata[0].args["name"] == "01_Test" - assert metadata[1].tid == 1 and metadata[1].args["sort_index"] == 2 + assert len(metadata) == 4 + assert metadata[2].tid == 1 and metadata[2].args["name"] == "01_Test" + assert metadata[3].tid == 1 and metadata[3].args["sort_index"] == 2 -def test_reuse_earliest_thread(): +def test_reuse_earliest_lane(): metadata = [] - manager = ThreadPoolManager( - max_width=2, pid=100, thread_name_prefix="Test", metadata_events=metadata + manager = StageLaneManager( + max_width=2, pid=100, lane_name_prefix="Test", metadata_events=metadata ) - manager.add_initial_thread(end_time=10) - manager.assign_thread(start_time=5, end_time=20) + manager.assign_lane(start_time=0, end_time=5) + manager.assign_lane(start_time=0, end_time=10) - pid, tid = manager.assign_thread(start_time=8, end_time=25) + pid, tid = manager.assign_lane(start_time=3, end_time=20) assert tid == 0 assert pid == 100 - assert manager.pool[0][0] == 25 - assert manager.pool[1][0] == 20 + assert manager.pool[0][0] == 20 + assert manager.pool[1][0] == 10