Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cache Draft4Validator instead of dict version of JSON schema. #154

Closed
OrangeTux opened this issue Nov 13, 2020 · 0 comments · Fixed by #155
Closed

Cache Draft4Validator instead of dict version of JSON schema. #154

OrangeTux opened this issue Nov 13, 2020 · 0 comments · Fixed by #155
Assignees

Comments

@OrangeTux
Copy link
Collaborator

OrangeTux commented Nov 13, 2020

I've done some micro benchmarks of OCPP and found that a few hot spots that are easy to optimize.

Currently OCPP caches the JSON schema as a Python dict. That prevents reading to disk and parsing the schema if a schema is used more than one time. But still every time a schema is used this dict has to be turned into a Draft4Validator instance

Caching the Draft4Validator instance reduces the time to handle a Call significantly.

I've used the following script to benchmark.
It requires pytest-benchmark. This package doesn't support async functions natively, but @mbello provided some code to fix that in this issue.

The script can be executed using `

$ pytest tests/test_benchmark.py
# tests/benchmark.py
import asyncio
import threading
import pytest
from datetime import datetime

from ocpp.routing import on
from ocpp.v16 import ChargePoint as cp, call_result
from ocpp.v16.enums import Action, RegistrationStatus
from ocpp.messages import Call


@pytest.fixture
def boot_notification_call():
    return Call(
        unique_id="1",
        action=Action.BootNotification,
        payload={
            'chargePointVendor': 'Alfen BV',
            'chargePointModel': 'ICU Eve Mini',
            'firmwareVersion': "#1:3.4.0-2990#N:217H;1.0-223",
        }).to_json()


@pytest.fixture
def meter_values_call():
    return Call(
        unique_id="1",
        action=Action.MeterValues,
        payload={
            'transactionId': 6998264,
            'connectorId': 1,
            'meterValue': [{
                'timestamp': '2020-07-09T12:52:40Z',
                'sampledValue': [{
                    'location': 'Outlet',
                    'measurand': 'Energy.Active.Import.Register',
                    'unit': 'Wh',
                    'value': '419780.000'
                }, {
                    'context': 'Sample.Periodic',
                    'location': 'Outlet',
                    'measurand': 'Energy.Active.Import.Register',
                    'phase': 'L1',
                    'unit': 'Wh',
                    'value': '242730.000'
                }, {
                    'context': 'Sample.Periodic',
                    'location': 'Outlet',
                    'measurand': 'Energy.Active.Import.Register',
                    'phase': 'L2',
                    'unit': 'Wh',
                    'value': '91370.000'
                }, {
                    'context': 'Sample.Periodic',
                    'location': 'Outlet',
                    'measurand': 'Energy.Active.Import.Register',
                    'phase': 'L3',
                    'unit': 'Wh',
                    'value': '85670.000'
                }, {
                    'context': 'Sample.Periodic',
                    'location': 'Outlet',
                    'measurand': 'Power.Active.Import',
                    'unit': 'W',
                    'value': '0.000'
                }, {
                    'context': 'Sample.Periodic',
                    'location': 'Outlet',
                    'measurand': 'Power.Active.Import',
                    'phase': 'L1',
                    'unit': 'W',
                    'value': '0.000'
                }, {
                    'context': 'Sample.Periodic',
                    'location': 'Outlet',
                    'measurand': 'Power.Active.Import',
                    'phase': 'L2',
                    'unit': 'W',
                    'value': '0.000'
                }, {
                    'context': 'Sample.Periodic',
                    'location': 'Outlet',
                    'measurand': 'Power.Active.Import',
                    'phase': 'L3',
                    'unit': 'W',
                    'value': '0.000'
                }, {
                    'context': 'Sample.Periodic',
                    'location': 'Outlet',
                    'measurand': 'Current.Offered',
                    'unit': 'A',
                    'value': '16.000'
                }, {
                    'context': 'Sample.Periodic',
                    'location': 'Outlet',
                    'measurand': 'Current.Import',
                    'phase': 'N',
                    'unit': 'A',
                    'value': '0.000'
                }, {
                    'context': 'Sample.Periodic',
                    'location': 'Outlet',
                    'measurand': 'Current.Import',
                    'phase': 'L1',
                    'unit': 'A',
                    'value': '0.000'
                }, {
                    'context': 'Sample.Periodic',
                    'location': 'Outlet',
                    'measurand': 'Current.Import',
                    'phase': 'L2',
                    'unit': 'A',
                    'value': '0.000'
                }, {
                    'context': 'Sample.Periodic',
                    'location': 'Outlet',
                    'measurand': 'Current.Import',
                    'phase': 'L3',
                    'unit': 'A',
                    'value': '0.000'
                }]
            }]
        }).to_json()


class Sync2Async:
    def __init__(self, coro, *args, **kwargs):
        self.coro = coro
        self.args = args
        self.kwargs = kwargs
        self.custom_loop = None
        self.thread = None

    def start_background_loop(self) -> None:
        asyncio.set_event_loop(self.custom_loop)
        self.custom_loop.run_forever()

    def __call__(self):
        awaitable = self.coro(*self.args, **self.kwargs)
        if (
                not self.custom_loop
                or not self.thread
                or not self.thread.is_alive()
        ):
            self.custom_loop = asyncio.new_event_loop()
            self.thread = threading.Thread(
                    target=self.start_background_loop, daemon=True
            )
            self.thread.start()

        return asyncio.run_coroutine_threadsafe(
            awaitable, self.custom_loop
        ).result()


def on_boot_notitication():
    return call_result.BootNotificationPayload(
        current_time=datetime.utcnow().isoformat(),
        interval=10,
        status=RegistrationStatus.accepted
    )


class ChargePointWithoutValidation(cp):
    @on(Action.BootNotification, skip_schema_validation=True)
    def on_boot_notitication(*args, **kwargs):
        return on_boot_notitication(*args, **kwargs)

    @on(Action.MeterValues, skip_schema_validation=True)
    def on_meter_values(*args, **kwargs):
        return call_result.MeterValuesPayload()


class ChargePointWithValidation(cp):
    @on(Action.BootNotification, skip_schema_validation=False)
    def on_boot_notitication(*args, **kwargs):
        return on_boot_notitication(*args, **kwargs)

    @on(Action.MeterValues, skip_schema_validation=False)
    def on_meter_values(*args, **kwargs):
        return call_result.MeterValuesPayload()


@pytest.mark.benchmark
@pytest.mark.asyncio
async def test_boot_notification_without_validation(benchmark, boot_notification_call, connection):
    cp = ChargePointWithoutValidation('TMH_000122', connection)

    benchmark(Sync2Async(cp.route_message, boot_notification_call))


@pytest.mark.benchmark
@pytest.mark.asyncio
async def test_meter_values_without_validation(benchmark, meter_values_call, connection):
    cp = ChargePointWithoutValidation('TMH_000122', connection)

    benchmark(Sync2Async(cp.route_message, meter_values_call))


@pytest.mark.benchmark
@pytest.mark.asyncio
async def test_boot_notification_with_validation(benchmark, boot_notification_call, connection):
    cp = ChargePointWithValidation('TMH_000122', connection)

    benchmark(Sync2Async(cp.route_message, boot_notification_call))


@pytest.mark.benchmark
@pytest.mark.asyncio
async def test_meter_values_with_validation(benchmark, meter_values_call, connection):
    cp = ChargePointWithValidation('TMH_000122', connection)

    benchmark(Sync2Async(cp.route_message, meter_values_call))


class Connection:
    def send(self, _):
        pass

Next I used py-spy to create a Flamegraph of the test run. That allowed me to find hot spots in the code.

You can use py-spy like this:

$ py-spy record -o profile.svg -- pytest tests/test_benchmark.py

That outputs a file profile.svg that you can inspect in the browser.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging a pull request may close this issue.

1 participant