Add benchmarks for speedups (#473)

* Add benchmarks for speedups

* Update README.md

* Update README.md

Co-authored-by: Teemu R. <tpr@iki.fi>

* relo

* Update README.md

* document benchmark

* Update README.md

---------

Co-authored-by: Teemu R. <tpr@iki.fi>
This commit is contained in:
J. Nick Koston 2023-07-01 18:03:50 -05:00 committed by GitHub
parent b83986bd51
commit fde156c859
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 231 additions and 0 deletions

View File

@ -23,6 +23,8 @@ If you are using cpython, it is recommended to install with `[speedups]` to enab
pip install python-kasa[speedups]
```
With `[speedups]`, the protocol overhead is roughly an order of magnitude lower (benchmarks available in devtools).
Alternatively, you can clone this repository and use poetry to install the development version:
```
git clone https://github.com/python-kasa/python-kasa.git

View File

@ -59,3 +59,13 @@ id
<id>-HS110(EU) 5.0 0.055700 0.016174 0.042086 0.045578 0.048905 0.059869 0.082064
<id>-KP303(UK) 5.0 0.010298 0.003765 0.007773 0.007968 0.008546 0.010439 0.016763
```
## benchmark
* Benchmark the protocol
```shell
% python3 devtools/bench/benchmark.py
New parser, parsing 100000 messages took 0.6339647499989951 seconds
Old parser, parsing 100000 messages took 9.473990250000497 seconds
```

View File

@ -0,0 +1,30 @@
"""Benchmark the new parser against the old parser."""
import json
import timeit
import orjson
from kasa_crypt import decrypt, encrypt
from utils.data import REQUEST, WIRE_RESPONSE
from utils.original import OriginalTPLinkSmartHomeProtocol
def original_request_response() -> None:
"""Benchmark the original parser."""
OriginalTPLinkSmartHomeProtocol.encrypt(json.dumps(REQUEST))
json.loads(OriginalTPLinkSmartHomeProtocol.decrypt(WIRE_RESPONSE[4:]))
def new_request_response() -> None:
"""Benchmark the new parser."""
encrypt(orjson.dumps(REQUEST).decode())
orjson.loads(decrypt(WIRE_RESPONSE[4:]))
count = 100000
time = timeit.Timer(new_request_response).timeit(count)
print(f"New parser, parsing {count} messages took {time} seconds")
time = timeit.Timer(original_request_response).timeit(count)
print(f"Old parser, parsing {count} messages took {time} seconds")

View File

@ -0,0 +1 @@
"""Benchmark utils."""

View File

@ -0,0 +1,141 @@
"""Test data for benchmarks."""
import json
from .original import OriginalTPLinkSmartHomeProtocol
REQUEST = {
"system": {"get_sysinfo": None},
"anti_theft": {"get_rules": None, "get_next_action": None},
"schedule": {
"get_rules": None,
"get_next_action": None,
"get_realtime": None,
"get_daystat": {"year": 2023, "month": 6},
"get_monthstat": {"year": 2023},
},
"time": {"get_time": None, "get_timezone": None},
"emeter": {
"get_realtime": None,
"get_daystat": {"year": 2023, "month": 6},
"get_monthstat": {"year": 2023},
},
}
RESPONSE = {
"anti_theft": {
"get_next_action": {"err_code": -2, "err_msg": "member not support"},
"get_rules": {"enable": 0, "err_code": 0, "rule_list": [], "version": 2},
},
"emeter": {
"get_daystat": {
"day_list": [{"day": 30, "energy_wh": 0, "month": 6, "year": 2023}],
"err_code": 0,
},
"get_monthstat": {
"err_code": 0,
"month_list": [{"energy_wh": 0, "month": 6, "year": 2023}],
},
"get_realtime": {
"current_ma": 0,
"err_code": 0,
"power_mw": 0,
"slot_id": 0,
"total_wh": 0,
"voltage_mv": 119390,
},
},
"schedule": {
"get_daystat": {
"day_list": [{"day": 30, "month": 6, "time": 3, "year": 2023}],
"err_code": 0,
},
"get_monthstat": {
"err_code": 0,
"month_list": [{"month": 6, "time": 3, "year": 2023}],
},
"get_next_action": {"err_code": 0, "type": -1},
"get_realtime": {"err_code": -2, "err_msg": "member not support"},
"get_rules": {"enable": 1, "err_code": 0, "rule_list": [], "version": 2},
},
"system": {
"get_sysinfo": {
"alias": "TP-LINK_Power Strip_5C33",
"child_num": 6,
"children": [
{
"alias": "Plug 1",
"id": "8006AF35494E7DB13DDE9B8F40BF2E001E77031900",
"next_action": {"type": -1},
"on_time": 231,
"state": 1,
},
{
"alias": "Plug 2",
"id": "8006AF35494E7DB13DDE9B8F40BF2E001E77031901",
"next_action": {"type": -1},
"on_time": 231,
"state": 1,
},
{
"alias": "Plug 3",
"id": "8006AF35494E7DB13DDE9B8F40BF2E001E77031902",
"next_action": {"type": -1},
"on_time": 231,
"state": 1,
},
{
"alias": "Plug 4",
"id": "8006AF35494E7DB13DDE9B8F40BF2E001E77031903",
"next_action": {"type": -1},
"on_time": 231,
"state": 1,
},
{
"alias": "Plug 5",
"id": "8006AF35494E7DB13DDE9B8F40BF2E001E77031904",
"next_action": {"type": -1},
"on_time": 231,
"state": 1,
},
{
"alias": "Plug 6",
"id": "8006AF35494E7DB13DDE9B8F40BF2E001E77031905",
"next_action": {"type": -1},
"on_time": 231,
"state": 1,
},
],
"deviceId": "8006AF35494E7DB13DDE9B8F40BF2E001E770319",
"err_code": 0,
"feature": "TIM:ENE",
"hwId": "955F433CBA24823A248A59AA64571A73",
"hw_ver": "2.0",
"latitude_i": 297852,
"led_off": 0,
"longitude_i": -954074,
"mac": "C0:06:C3:42:5C:33",
"mic_type": "IOT.SMARTPLUGSWITCH",
"model": "HS300(US)",
"oemId": "32BD0B21AA9BF8E84737D1DB1C66E883",
"rssi": -41,
"status": "new",
"sw_ver": "1.0.3 Build 201203 Rel.165457",
"updating": 0,
}
},
"time": {
"get_time": {
"err_code": 0,
"hour": 9,
"mday": 30,
"min": 32,
"month": 6,
"sec": 54,
"year": 2023,
},
"get_timezone": {"err_code": 0, "index": 13},
},
}
WIRE_RESPONSE = OriginalTPLinkSmartHomeProtocol.encrypt(json.dumps(RESPONSE))

View File

@ -0,0 +1,47 @@
"""Original implementation of the TP-Link Smart Home protocol."""
import struct
from typing import Generator
class OriginalTPLinkSmartHomeProtocol:
"""Original implementation of the TP-Link Smart Home protocol."""
INITIALIZATION_VECTOR = 171
@staticmethod
def _xor_payload(unencrypted: bytes) -> Generator[int, None, None]:
key = OriginalTPLinkSmartHomeProtocol.INITIALIZATION_VECTOR
for unencryptedbyte in unencrypted:
key = key ^ unencryptedbyte
yield key
@staticmethod
def encrypt(request: str) -> bytes:
"""Encrypt a request for a TP-Link Smart Home Device.
:param request: plaintext request data
:return: ciphertext to be send over wire, in bytes
"""
plainbytes = request.encode()
return struct.pack(">I", len(plainbytes)) + bytes(
OriginalTPLinkSmartHomeProtocol._xor_payload(plainbytes)
)
@staticmethod
def _xor_encrypted_payload(ciphertext: bytes) -> Generator[int, None, None]:
key = OriginalTPLinkSmartHomeProtocol.INITIALIZATION_VECTOR
for cipherbyte in ciphertext:
plainbyte = key ^ cipherbyte
key = cipherbyte
yield plainbyte
@staticmethod
def decrypt(ciphertext: bytes) -> str:
"""Decrypt a response of a TP-Link Smart Home Device.
:param ciphertext: encrypted response data
:return: plaintext response
"""
return bytes(
OriginalTPLinkSmartHomeProtocol._xor_encrypted_payload(ciphertext)
).decode()