88import sys
99import time
1010from contextlib import suppress
11+ from dataclasses import dataclass
1112from typing import TYPE_CHECKING
1213from typing import Any
1314
1415import click
1516from rich .table import Table
16- from sqlalchemy .orm import Mapped
17- from sqlalchemy .orm import mapped_column
1817
1918from _pytask .click import ColoredCommand
2019from _pytask .click import EnumChoice
2120from _pytask .console import console
2221from _pytask .console import format_task_name
2322from _pytask .dag import create_dag
24- from _pytask .database_utils import BaseTable
25- from _pytask .database_utils import DatabaseSession
2623from _pytask .exceptions import CollectionError
2724from _pytask .exceptions import ConfigurationError
2825from _pytask .node_protocols import PPathNode
3128from _pytask .outcomes import TaskOutcome
3229from _pytask .pluginmanager import hookimpl
3330from _pytask .pluginmanager import storage
31+ from _pytask .runtime_store import RuntimeState
3432from _pytask .session import Session
3533from _pytask .traceback import Traceback
3634
@@ -48,16 +46,6 @@ class _ExportFormats(enum.Enum):
4846 CSV = "csv"
4947
5048
51- class Runtime (BaseTable ):
52- """Record of runtimes of tasks."""
53-
54- __tablename__ = "runtime"
55-
56- task : Mapped [str ] = mapped_column (primary_key = True )
57- date : Mapped [float ]
58- duration : Mapped [float ]
59-
60-
6149@hookimpl (tryfirst = True )
6250def pytask_extend_command_line_interface (cli : click .Group ) -> None :
6351 """Extend the command line interface."""
@@ -67,8 +55,10 @@ def pytask_extend_command_line_interface(cli: click.Group) -> None:
6755@hookimpl
6856def pytask_post_parse (config : dict [str , Any ]) -> None :
6957 """Register the export option."""
58+ runtime_state = RuntimeState .from_root (config ["root" ])
59+ config ["pm" ].register (ProfilePlugin (runtime_state ))
60+ config ["pm" ].register (DurationNameSpace (runtime_state ))
7061 config ["pm" ].register (ExportNameSpace )
71- config ["pm" ].register (DurationNameSpace )
7262 config ["pm" ].register (FileSizeNameSpace )
7363
7464
@@ -82,27 +72,50 @@ def pytask_execute_task(task: PTask) -> Generator[None, None, None]:
8272 return result
8373
8474
85- @hookimpl
86- def pytask_execute_task_process_report (report : ExecutionReport ) -> None :
87- """Store runtime of successfully finishing tasks in database."""
88- task = report .task
89- duration = task .attributes .get ("duration" )
90- if report .outcome == TaskOutcome .SUCCESS and duration is not None :
91- _create_or_update_runtime (task .signature , * duration )
75+ @dataclass
76+ class ProfilePlugin :
77+ """Collect and persist runtime profiling data."""
78+
79+ runtime_state : RuntimeState
80+
81+ @hookimpl
82+ def pytask_execute_task_process_report (
83+ self , session : Session , report : ExecutionReport
84+ ) -> None :
85+ """Store runtime of successfully finishing tasks."""
86+ _ = session
87+ task = report .task
88+ duration = task .attributes .get ("duration" )
89+ if report .outcome == TaskOutcome .SUCCESS and duration is not None :
90+ self .runtime_state .update_task (task , * duration )
9291
92+ @hookimpl
93+ def pytask_unconfigure (self , session : Session ) -> None :
94+ """Flush runtime information on normal build exits."""
95+ if session .config .get ("command" ) != "build" :
96+ return
97+ if session .config .get ("dry_run" ) or session .config .get ("explain" ):
98+ return
99+ self .runtime_state .flush ()
93100
94- def _create_or_update_runtime (task_signature : str , start : float , end : float ) -> None :
95- """Create or update a runtime entry."""
96- with DatabaseSession () as session :
97- runtime = session .get (Runtime , task_signature )
98101
99- if not runtime :
100- session .add (Runtime (task = task_signature , date = start , duration = end - start ))
101- else :
102- for attr , val in (("date" , start ), ("duration" , end - start )):
103- setattr (runtime , attr , val )
102+ @dataclass
103+ class DurationNameSpace :
104+ """A namespace for adding durations to the profile."""
105+
106+ def __init__ (self , runtime_state : RuntimeState ) -> None :
107+ self .runtime_state = runtime_state
104108
105- session .commit ()
109+ @hookimpl
110+ def pytask_profile_add_info_on_task (
111+ self , session : Session , tasks : list [PTask ], profile : dict [str , dict [str , Any ]]
112+ ) -> None :
113+ """Add the runtime for tasks to the profile."""
114+ _ = session
115+ for task in tasks :
116+ duration = self .runtime_state .get_duration (task )
117+ if duration is not None :
118+ profile [task .name ]["Duration (in s)" ] = round (duration , 2 )
106119
107120
108121@click .command (cls = ColoredCommand )
@@ -183,29 +196,6 @@ def _print_profile_table(
183196 console .print ("No information is stored on the collected tasks." )
184197
185198
186- class DurationNameSpace :
187- """A namespace for adding durations to the profile."""
188-
189- @staticmethod
190- @hookimpl
191- def pytask_profile_add_info_on_task (
192- tasks : list [PTask ], profile : dict [str , dict [str , Any ]]
193- ) -> None :
194- """Add the runtime for tasks to the profile."""
195- runtimes = _collect_runtimes (tasks )
196- for name , duration in runtimes .items ():
197- profile [name ]["Duration (in s)" ] = round (duration , 2 )
198-
199-
200- def _collect_runtimes (tasks : list [PTask ]) -> dict [str , float ]:
201- """Collect runtimes."""
202- with DatabaseSession () as session :
203- runtimes = [session .get (Runtime , task .signature ) for task in tasks ]
204- return {
205- task .name : r .duration for task , r in zip (tasks , runtimes , strict = False ) if r
206- }
207-
208-
209199class FileSizeNameSpace :
210200 """A namespace for adding the total file size of products to a task."""
211201
0 commit comments