forked from airbytehq/airbyte
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Output run time per stream (airbytehq#8173)
* Output run time per stream * Code review changes: Remove multithreaded support. Port code to cdk * Formatting * remove extraneous try block
- Loading branch information
Showing
6 changed files
with
321 additions
and
27 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
# | ||
# Copyright (c) 2021 Airbyte, Inc., all rights reserved. | ||
# | ||
|
||
import datetime | ||
import time | ||
from contextlib import contextmanager | ||
from dataclasses import dataclass, field | ||
|
||
from airbyte_cdk.logger import AirbyteLogger | ||
|
||
logger = AirbyteLogger() | ||
|
||
|
||
class EventTimer: | ||
"""Simple nanosecond resolution event timer for debugging, initially intended to be used to record streams execution | ||
time for a source. | ||
Event nesting follows a LIFO pattern, so finish will apply to the last started event. | ||
""" | ||
|
||
def __init__(self, name): | ||
self.name = name | ||
self.events = {} | ||
self.count = 0 | ||
self.stack = [] | ||
|
||
def start_event(self, name): | ||
""" | ||
Start a new event and push it to the stack. | ||
""" | ||
self.events[name] = Event(name=name) | ||
self.count += 1 | ||
self.stack.insert(0, self.events[name]) | ||
|
||
def finish_event(self): | ||
""" | ||
Finish the current event and pop it from the stack. | ||
""" | ||
|
||
if self.stack: | ||
event = self.stack.pop(0) | ||
event.finish() | ||
else: | ||
logger.warn(f"{self.name} finish_event called without start_event") | ||
|
||
def report(self, order_by="name"): | ||
""" | ||
:param order_by: 'name' or 'duration' | ||
""" | ||
if order_by == "name": | ||
events = sorted(self.events.values(), key=lambda event: event.name) | ||
elif order_by == "duration": | ||
events = sorted(self.events.values(), key=lambda event: event.duration) | ||
text = f"{self.name} runtimes:\n" | ||
text += "\n".join(str(event) for event in events) | ||
return text | ||
|
||
|
||
@dataclass | ||
class Event: | ||
name: str | ||
start: float = field(default_factory=time.perf_counter_ns) | ||
end: float = field(default=None) | ||
|
||
@property | ||
def duration(self) -> float: | ||
"""Returns the elapsed time in seconds or positive infinity if event was never finished""" | ||
if self.end: | ||
return (self.end - self.start) / 1e9 | ||
return float("+inf") | ||
|
||
def __str__(self): | ||
return f"{self.name} {datetime.timedelta(seconds=self.duration)}" | ||
|
||
def finish(self): | ||
self.end = time.perf_counter_ns() | ||
|
||
|
||
@contextmanager | ||
def create_timer(name): | ||
""" | ||
Creates a new EventTimer as a context manager to improve code readability. | ||
""" | ||
a_timer = EventTimer(name) | ||
yield a_timer |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
# | ||
# Copyright (c) 2021 Airbyte, Inc., all rights reserved. | ||
# | ||
|
||
|
||
from unittest import mock | ||
|
||
from airbyte_cdk.utils.event_timing import create_timer | ||
|
||
|
||
def test_counter_init(): | ||
with create_timer("Counter") as timer: | ||
assert timer.name == "Counter" | ||
|
||
|
||
def test_counter_start_event(): | ||
with create_timer("Counter") as timer: | ||
with mock.patch("airbyte_cdk.utils.event_timing.EventTimer.start_event") as mock_start_event: | ||
timer.start_event("test_event") | ||
mock_start_event.assert_called_with("test_event") | ||
|
||
|
||
def test_counter_finish_event(): | ||
with create_timer("Counter") as timer: | ||
with mock.patch("airbyte_cdk.utils.event_timing.EventTimer.finish_event") as mock_finish_event: | ||
timer.finish_event("test_event") | ||
mock_finish_event.assert_called_with("test_event") | ||
|
||
|
||
def test_timer_multiple_events(): | ||
with create_timer("Counter") as timer: | ||
for i in range(10): | ||
timer.start_event("test_event") | ||
timer.finish_event() | ||
assert timer.count == 10 | ||
|
||
|
||
def test_report_is_ordered_by_name_by_default(): | ||
names = ["j", "b", "g", "d", "e", "f", "c", "h", "i", "a"] | ||
|
||
with create_timer("Source Counter") as timer: | ||
for name in names: | ||
timer.start_event(name) | ||
timer.finish_event() | ||
report = timer.report().split("\n")[1:] # ignore the first line | ||
report_names = [line.split(" ")[0] for line in report] | ||
assert report_names == sorted(names) | ||
|
||
|
||
def test_double_finish_is_safely_ignored(): | ||
with create_timer("Source Counter") as timer: | ||
timer.start_event("test_event") | ||
timer.finish_event() | ||
timer.finish_event() | ||
assert timer.count == 1 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
85 changes: 85 additions & 0 deletions
85
airbyte-integrations/bases/base-python/base_python/cdk/utils/event_timing.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
# | ||
# Copyright (c) 2021 Airbyte, Inc., all rights reserved. | ||
# | ||
|
||
import datetime | ||
import time | ||
from contextlib import contextmanager | ||
from dataclasses import dataclass, field | ||
|
||
from base_python.logger import AirbyteLogger | ||
|
||
logger = AirbyteLogger() | ||
|
||
|
||
class EventTimer: | ||
"""Simple nanosecond resolution event timer for debugging, initially intended to be used to record streams execution | ||
time for a source. | ||
Event nesting follows a LIFO pattern, so finish will apply to the last started event. | ||
""" | ||
|
||
def __init__(self, name): | ||
self.name = name | ||
self.events = {} | ||
self.count = 0 | ||
self.stack = [] | ||
|
||
def start_event(self, name): | ||
""" | ||
Start a new event and push it to the stack. | ||
""" | ||
self.events[name] = Event(name=name) | ||
self.count += 1 | ||
self.stack.insert(0, self.events[name]) | ||
|
||
def finish_event(self): | ||
""" | ||
Finish the current event and pop it from the stack. | ||
""" | ||
|
||
if self.stack: | ||
event = self.stack.pop(0) | ||
event.finish() | ||
else: | ||
logger.warn(f"{self.name} finish_event called without start_event") | ||
|
||
def report(self, order_by="name"): | ||
""" | ||
:param order_by: 'name' or 'duration' | ||
""" | ||
if order_by == "name": | ||
events = sorted(self.events.values(), key=lambda event: event.name) | ||
elif order_by == "duration": | ||
events = sorted(self.events.values(), key=lambda event: event.duration) | ||
text = f"{self.name} runtimes:\n" | ||
text += "\n".join(str(event) for event in events) | ||
return text | ||
|
||
|
||
@dataclass | ||
class Event: | ||
name: str | ||
start: float = field(default_factory=time.perf_counter_ns) | ||
end: float = field(default=None) | ||
|
||
@property | ||
def duration(self) -> float: | ||
"""Returns the elapsed time in seconds or positive infinity if event was never finished""" | ||
if self.end: | ||
return (self.end - self.start) / 1e9 | ||
return float("+inf") | ||
|
||
def __str__(self): | ||
return f"{self.name} {datetime.timedelta(seconds=self.duration)}" | ||
|
||
def finish(self): | ||
self.end = time.perf_counter_ns() | ||
|
||
|
||
@contextmanager | ||
def create_timer(name): | ||
""" | ||
Creates a new EventTimer as a context manager to improve code readability. | ||
""" | ||
a_timer = EventTimer(name) | ||
yield a_timer |
54 changes: 54 additions & 0 deletions
54
airbyte-integrations/bases/base-python/unit_tests/test_counter.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
# | ||
# Copyright (c) 2021 Airbyte, Inc., all rights reserved. | ||
# | ||
|
||
from unittest import mock | ||
|
||
from base_python.cdk.utils.event_timing import create_timer | ||
|
||
|
||
def test_counter_init(): | ||
with create_timer("Counter") as timer: | ||
assert timer.name == "Counter" | ||
|
||
|
||
def test_counter_start_event(): | ||
with create_timer("Counter") as timer: | ||
with mock.patch("base_python.cdk.utils.event_timing.EventTimer.start_event") as mock_start_event: | ||
timer.start_event("test_event") | ||
mock_start_event.assert_called_with("test_event") | ||
|
||
|
||
def test_counter_finish_event(): | ||
with create_timer("Counter") as timer: | ||
with mock.patch("base_python.cdk.utils.event_timing.EventTimer.finish_event") as mock_finish_event: | ||
timer.finish_event("test_event") | ||
mock_finish_event.assert_called_with("test_event") | ||
|
||
|
||
def test_timer_multiple_events(): | ||
with create_timer("Counter") as timer: | ||
for i in range(10): | ||
timer.start_event("test_event") | ||
timer.finish_event() | ||
assert timer.count == 10 | ||
|
||
|
||
def test_report_is_ordered_by_name_by_default(): | ||
names = ["j", "b", "g", "d", "e", "f", "c", "h", "i", "a"] | ||
|
||
with create_timer("Source Counter") as timer: | ||
for name in names: | ||
timer.start_event(name) | ||
timer.finish_event() | ||
report = timer.report().split("\n")[1:] # ignore the first line | ||
report_names = [line.split(" ")[0] for line in report] | ||
assert report_names == sorted(names) | ||
|
||
|
||
def test_double_finish_is_safely_ignored(): | ||
with create_timer("Source Counter") as timer: | ||
timer.start_event("test_event") | ||
timer.finish_event() | ||
timer.finish_event() | ||
assert timer.count == 1 |