Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """Script to run benchmarks."""
2 
3 from __future__ import annotations
4 
5 import argparse
6 import asyncio
7 from collections.abc import Callable
8 from contextlib import suppress
9 import logging
10 from timeit import default_timer as timer
11 
12 from homeassistant import core
13 from homeassistant.const import EVENT_STATE_CHANGED
14 from homeassistant.helpers.entityfilter import convert_include_exclude_filter
15 from homeassistant.helpers.event import (
16  async_track_state_change,
17  async_track_state_change_event,
18 )
19 from homeassistant.helpers.json import JSON_DUMP
20 
21 # mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs
22 # mypy: no-warn-return-any
23 
24 BENCHMARKS: dict[str, Callable] = {}
25 
26 
27 def run(args):
28  """Handle benchmark commandline script."""
29  # Disable logging
30  logging.getLogger("homeassistant.core").setLevel(logging.CRITICAL)
31 
32  parser = argparse.ArgumentParser(description="Run a Home Assistant benchmark.")
33  parser.add_argument("name", choices=BENCHMARKS)
34  parser.add_argument("--script", choices=["benchmark"])
35 
36  args = parser.parse_args()
37 
38  bench = BENCHMARKS[args.name]
39  print("Using event loop:", asyncio.get_event_loop_policy().loop_name)
40 
41  with suppress(KeyboardInterrupt):
42  while True:
43  asyncio.run(run_benchmark(bench))
44 
45 
46 async def run_benchmark(bench):
47  """Run a benchmark."""
48  hass = core.HomeAssistant("")
49  runtime = await bench(hass)
50  print(f"Benchmark {bench.__name__} done in {runtime}s")
51  await hass.async_stop()
52 
53 
54 def benchmark[_CallableT: Callable](func: _CallableT) -> _CallableT:
55  """Decorate to mark a benchmark."""
56  BENCHMARKS[func.__name__] = func
57  return func
58 
59 
60 @benchmark
61 async def fire_events(hass):
62  """Fire a million events."""
63  count = 0
64  event_name = "benchmark_event"
65  events_to_fire = 10**6
66 
67  @core.callback
68  def listener(_):
69  """Handle event."""
70  nonlocal count
71  count += 1
72 
73  hass.bus.async_listen(event_name, listener)
74 
75  for _ in range(events_to_fire):
76  hass.bus.async_fire(event_name)
77 
78  start = timer()
79 
80  await hass.async_block_till_done()
81 
82  assert count == events_to_fire
83 
84  return timer() - start
85 
86 
87 @benchmark
88 async def fire_events_with_filter(hass):
89  """Fire a million events with a filter that rejects them."""
90  count = 0
91  event_name = "benchmark_event"
92  events_to_fire = 10**6
93 
94  @core.callback
95  def event_filter(event_data):
96  """Filter event."""
97  return False
98 
99  @core.callback
100  def listener(_):
101  """Handle event."""
102  nonlocal count
103  count += 1
104 
105  hass.bus.async_listen(event_name, listener, event_filter=event_filter)
106 
107  for _ in range(events_to_fire):
108  hass.bus.async_fire(event_name)
109 
110  start = timer()
111 
112  await hass.async_block_till_done()
113 
114  assert count == 0
115 
116  return timer() - start
117 
118 
119 @benchmark
120 async def state_changed_helper(hass):
121  """Run a million events through state changed helper with 1000 entities."""
122  count = 0
123  entity_id = "light.kitchen"
124  event = asyncio.Event()
125 
126  @core.callback
127  def listener(*args):
128  """Handle event."""
129  nonlocal count
130  count += 1
131 
132  if count == 10**6:
133  event.set()
134 
135  for idx in range(1000):
136  async_track_state_change(hass, f"{entity_id}{idx}", listener, "off", "on")
137  event_data = {
138  "entity_id": f"{entity_id}0",
139  "old_state": core.State(entity_id, "off"),
140  "new_state": core.State(entity_id, "on"),
141  }
142 
143  for _ in range(10**6):
144  hass.bus.async_fire(EVENT_STATE_CHANGED, event_data)
145 
146  start = timer()
147 
148  await event.wait()
149 
150  return timer() - start
151 
152 
153 @benchmark
155  """Run a million events through state changed event helper with 1000 entities."""
156  count = 0
157  entity_id = "light.kitchen"
158  events_to_fire = 10**6
159 
160  @core.callback
161  def listener(*args):
162  """Handle event."""
163  nonlocal count
164  count += 1
165 
167  hass, [f"{entity_id}{idx}" for idx in range(1000)], listener
168  )
169 
170  event_data = {
171  "entity_id": f"{entity_id}0",
172  "old_state": core.State(entity_id, "off"),
173  "new_state": core.State(entity_id, "on"),
174  }
175 
176  for _ in range(events_to_fire):
177  hass.bus.async_fire(EVENT_STATE_CHANGED, event_data)
178 
179  start = timer()
180 
181  await hass.async_block_till_done()
182 
183  assert count == events_to_fire
184 
185  return timer() - start
186 
187 
188 @benchmark
190  """Run a million events through state changed event helper.
191 
192  With 1000 entities that all get filtered.
193  """
194  count = 0
195  entity_id = "light.kitchen"
196  events_to_fire = 10**6
197 
198  @core.callback
199  def listener(*args):
200  """Handle event."""
201  nonlocal count
202  count += 1
203 
205  hass, [f"{entity_id}{idx}" for idx in range(1000)], listener
206  )
207 
208  event_data = {
209  "entity_id": "switch.no_listeners",
210  "old_state": core.State(entity_id, "off"),
211  "new_state": core.State(entity_id, "on"),
212  }
213 
214  for _ in range(events_to_fire):
215  hass.bus.async_fire(EVENT_STATE_CHANGED, event_data)
216 
217  start = timer()
218 
219  await hass.async_block_till_done()
220 
221  assert count == 0
222 
223  return timer() - start
224 
225 
226 @benchmark
227 async def filtering_entity_id(hass):
228  """Run a 100k state changes through entity filter."""
229  config = {
230  "include": {
231  "domains": [
232  "automation",
233  "script",
234  "group",
235  "media_player",
236  "custom_component",
237  ],
238  "entity_globs": [
239  "binary_sensor.*_contact",
240  "binary_sensor.*_occupancy",
241  "binary_sensor.*_detected",
242  "binary_sensor.*_active",
243  "input_*",
244  "device_tracker.*_phone",
245  "switch.*_light",
246  "binary_sensor.*_charging",
247  "binary_sensor.*_lock",
248  "binary_sensor.*_connected",
249  ],
250  "entities": [
251  "test.entity_1",
252  "test.entity_2",
253  "binary_sensor.garage_door_open",
254  "test.entity_3",
255  "test.entity_4",
256  ],
257  },
258  "exclude": {
259  "domains": ["input_number"],
260  "entity_globs": ["media_player.google_*", "group.all_*"],
261  "entities": [],
262  },
263  }
264 
265  entity_ids = [
266  "automation.home_arrival",
267  "script.shut_off_house",
268  "binary_sensor.garage_door_open",
269  "binary_sensor.front_door_lock",
270  "binary_sensor.kitchen_motion_sensor_occupancy",
271  "switch.desk_lamp",
272  "light.dining_room",
273  "input_boolean.guest_staying_over",
274  "person.eleanor_fant",
275  "alert.issue_at_home",
276  "calendar.eleanor_fant_s_calendar",
277  "sun.sun",
278  ]
279 
280  entities_filter = convert_include_exclude_filter(config)
281  size = len(entity_ids)
282 
283  start = timer()
284 
285  for i in range(10**5):
286  entities_filter(entity_ids[i % size])
287 
288  return timer() - start
289 
290 
291 @benchmark
292 async def valid_entity_id(hass):
293  """Run valid entity ID a million times."""
294  start = timer()
295  for _ in range(10**6):
296  core.valid_entity_id("light.kitchen")
297  return timer() - start
298 
299 
300 @benchmark
301 async def json_serialize_states(hass):
302  """Serialize million states with websocket default encoder."""
303  states = [
304  core.State("light.kitchen", "on", {"friendly_name": "Kitchen Lights"})
305  for _ in range(10**6)
306  ]
307 
308  start = timer()
309  JSON_DUMP(states)
310  return timer() - start
EntityFilter convert_include_exclude_filter(dict[str, dict[str, list[str]]] config)
CALLBACK_TYPE async_track_state_change_event(HomeAssistant hass, str|Iterable[str] entity_ids, Callable[[Event[EventStateChangedData]], Any] action, HassJobType|None job_type=None)
Definition: event.py:314
CALLBACK_TYPE async_track_state_change(HomeAssistant hass, str|Iterable[str] entity_ids, Callable[[str, State|None, State|None], Coroutine[Any, Any, None]|None] action, str|Iterable[str]|None from_state=None, str|Iterable[str]|None to_state=None)
Definition: event.py:211
def state_changed_event_filter_helper(hass)
Definition: __init__.py:189