Add type hints to synmark. (#16421)

This commit is contained in:
Patrick Cloke 2023-10-04 13:53:04 -04:00 committed by GitHub
parent 80ec81dcc5
commit ab9c1e8f39
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 91 additions and 49 deletions

1
changelog.d/16421.misc Normal file
View File

@ -0,0 +1 @@
Improve type hints.

View File

@ -32,6 +32,7 @@ files =
docker/, docker/,
scripts-dev/, scripts-dev/,
synapse/, synapse/,
synmark/,
tests/, tests/,
build_rust.py build_rust.py
@ -80,6 +81,9 @@ ignore_missing_imports = True
[mypy-pympler.*] [mypy-pympler.*]
ignore_missing_imports = True ignore_missing_imports = True
[mypy-pyperf.*]
ignore_missing_imports = True
[mypy-rust_python_jaeger_reporter.*] [mypy-rust_python_jaeger_reporter.*]
ignore_missing_imports = True ignore_missing_imports = True

View File

@ -13,15 +13,18 @@
# limitations under the License. # limitations under the License.
import sys import sys
from typing import cast
from synapse.types import ISynapseReactor
try: try:
from twisted.internet.epollreactor import EPollReactor as Reactor from twisted.internet.epollreactor import EPollReactor as Reactor
except ImportError: except ImportError:
from twisted.internet.pollreactor import PollReactor as Reactor from twisted.internet.pollreactor import PollReactor as Reactor # type: ignore[assignment]
from twisted.internet.main import installReactor from twisted.internet.main import installReactor
def make_reactor(): def make_reactor() -> ISynapseReactor:
""" """
Instantiate and install a Twisted reactor suitable for testing (i.e. not the Instantiate and install a Twisted reactor suitable for testing (i.e. not the
default global one). default global one).
@ -32,4 +35,4 @@ def make_reactor():
del sys.modules["twisted.internet.reactor"] del sys.modules["twisted.internet.reactor"]
installReactor(reactor) installReactor(reactor)
return reactor return cast(ISynapseReactor, reactor)

View File

@ -12,9 +12,10 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import sys import sys
from argparse import REMAINDER from argparse import REMAINDER, Namespace
from contextlib import redirect_stderr from contextlib import redirect_stderr
from io import StringIO from io import StringIO
from typing import Any, Callable, Coroutine, List, TypeVar
import pyperf import pyperf
@ -22,44 +23,50 @@ from twisted.internet.defer import Deferred, ensureDeferred
from twisted.logger import globalLogBeginner, textFileLogObserver from twisted.logger import globalLogBeginner, textFileLogObserver
from twisted.python.failure import Failure from twisted.python.failure import Failure
from synapse.types import ISynapseReactor
from synmark import make_reactor from synmark import make_reactor
from synmark.suites import SUITES from synmark.suites import SUITES
from tests.utils import setupdb from tests.utils import setupdb
T = TypeVar("T")
def make_test(main):
def make_test(
main: Callable[[ISynapseReactor, int], Coroutine[Any, Any, float]]
) -> Callable[[int], float]:
""" """
Take a benchmark function and wrap it in a reactor start and stop. Take a benchmark function and wrap it in a reactor start and stop.
""" """
def _main(loops): def _main(loops: int) -> float:
reactor = make_reactor() reactor = make_reactor()
file_out = StringIO() file_out = StringIO()
with redirect_stderr(file_out): with redirect_stderr(file_out):
d = Deferred() d: "Deferred[float]" = Deferred()
d.addCallback(lambda _: ensureDeferred(main(reactor, loops))) d.addCallback(lambda _: ensureDeferred(main(reactor, loops)))
def on_done(_): def on_done(res: T) -> T:
if isinstance(_, Failure): if isinstance(res, Failure):
_.printTraceback() res.printTraceback()
print(file_out.getvalue()) print(file_out.getvalue())
reactor.stop() reactor.stop()
return _ return res
d.addBoth(on_done) d.addBoth(on_done)
reactor.callWhenRunning(lambda: d.callback(True)) reactor.callWhenRunning(lambda: d.callback(True))
reactor.run() reactor.run()
return d.result # mypy thinks this is an object for some reason.
return d.result # type: ignore[return-value]
return _main return _main
if __name__ == "__main__": if __name__ == "__main__":
def add_cmdline_args(cmd, args): def add_cmdline_args(cmd: List[str], args: Namespace) -> None:
if args.log: if args.log:
cmd.extend(["--log"]) cmd.extend(["--log"])
cmd.extend(args.tests) cmd.extend(args.tests)
@ -82,17 +89,26 @@ if __name__ == "__main__":
setupdb() setupdb()
if runner.args.tests: if runner.args.tests:
SUITES = list( existing_suites = {s.__name__.split(".")[-1] for s, _ in SUITES}
filter(lambda x: x[0].__name__.split(".")[-1] in runner.args.tests, SUITES) for test in runner.args.tests:
) if test not in existing_suites:
print(f"Test suite {test} does not exist.")
exit(-1)
for suite, loops in SUITES: suites = list(
filter(lambda t: t[0].__name__.split(".")[-1] in runner.args.tests, SUITES)
)
else:
suites = SUITES
for suite, loops in suites:
if loops: if loops:
runner.args.loops = loops runner.args.loops = loops
loops_desc = str(loops)
else: else:
runner.args.loops = orig_loops runner.args.loops = orig_loops
loops = "auto" loops_desc = "auto"
runner.bench_time_func( runner.bench_time_func(
suite.__name__ + "_" + str(loops), suite.__name__ + "_" + loops_desc,
make_test(suite.main), make_test(suite.main),
) )

View File

@ -11,14 +11,16 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import logging import logging
import logging.config
import warnings import warnings
from io import StringIO from io import StringIO
from typing import Optional
from unittest.mock import Mock from unittest.mock import Mock
from pyperf import perf_counter from pyperf import perf_counter
from twisted.internet.address import IPv4Address, IPv6Address
from twisted.internet.defer import Deferred from twisted.internet.defer import Deferred
from twisted.internet.protocol import ServerFactory from twisted.internet.protocol import ServerFactory
from twisted.logger import LogBeginner, LogPublisher from twisted.logger import LogBeginner, LogPublisher
@ -26,45 +28,53 @@ from twisted.protocols.basic import LineOnlyReceiver
from synapse.config.logger import _setup_stdlib_logging from synapse.config.logger import _setup_stdlib_logging
from synapse.logging import RemoteHandler from synapse.logging import RemoteHandler
from synapse.synapse_rust import reset_logging_config
from synapse.types import ISynapseReactor
from synapse.util import Clock from synapse.util import Clock
class LineCounter(LineOnlyReceiver): class LineCounter(LineOnlyReceiver):
delimiter = b"\n" delimiter = b"\n"
count = 0
def __init__(self, *args, **kwargs): def lineReceived(self, line: bytes) -> None:
self.count = 0
super().__init__(*args, **kwargs)
def lineReceived(self, line):
self.count += 1 self.count += 1
assert isinstance(self.factory, Factory)
if self.count >= self.factory.wait_for and self.factory.on_done: if self.count >= self.factory.wait_for and self.factory.on_done:
on_done = self.factory.on_done on_done = self.factory.on_done
self.factory.on_done = None self.factory.on_done = None
on_done.callback(True) on_done.callback(True)
async def main(reactor, loops): class Factory(ServerFactory):
protocol = LineCounter
wait_for: int
on_done: Optional[Deferred]
async def main(reactor: ISynapseReactor, loops: int) -> float:
""" """
Benchmark how long it takes to send `loops` messages. Benchmark how long it takes to send `loops` messages.
""" """
servers = []
def protocol(): logger_factory = Factory()
p = LineCounter()
servers.append(p)
return p
logger_factory = ServerFactory.forProtocol(protocol)
logger_factory.wait_for = loops logger_factory.wait_for = loops
logger_factory.on_done = Deferred() logger_factory.on_done = Deferred()
port = reactor.listenTCP(0, logger_factory, interface="127.0.0.1") port = reactor.listenTCP(0, logger_factory, backlog=50, interface="127.0.0.1")
# A fake homeserver config. # A fake homeserver config.
class Config: class Config:
server_name = "synmark-" + str(loops) class server:
no_redirect_stdio = True server_name = "synmark-" + str(loops)
# This odd construct is to avoid mypy thinking that logging escapes the
# scope of Config.
class _logging:
no_redirect_stdio = True
logging = _logging
hs_config = Config() hs_config = Config()
@ -78,28 +88,34 @@ async def main(reactor, loops):
publisher, errors, mock_sys, warnings, initialBufferSize=loops publisher, errors, mock_sys, warnings, initialBufferSize=loops
) )
address = port.getHost()
assert isinstance(address, (IPv4Address, IPv6Address))
log_config = { log_config = {
"version": 1, "version": 1,
"loggers": {"synapse": {"level": "DEBUG", "handlers": ["tersejson"]}}, "loggers": {"synapse": {"level": "DEBUG", "handlers": ["remote"]}},
"formatters": {"tersejson": {"class": "synapse.logging.TerseJsonFormatter"}}, "formatters": {"tersejson": {"class": "synapse.logging.TerseJsonFormatter"}},
"handlers": { "handlers": {
"tersejson": { "remote": {
"class": "synapse.logging.RemoteHandler", "class": "synapse.logging.RemoteHandler",
"host": "127.0.0.1", "formatter": "tersejson",
"port": port.getHost().port, "host": address.host,
"port": address.port,
"maximum_buffer": 100, "maximum_buffer": 100,
"_reactor": reactor,
} }
}, },
} }
logger = logging.getLogger("synapse.logging.test_terse_json") logger = logging.getLogger("synapse")
_setup_stdlib_logging( _setup_stdlib_logging(
hs_config, hs_config, # type: ignore[arg-type]
log_config, None,
logBeginner=beginner, logBeginner=beginner,
) )
# Force a new logging config without having to load it from a file.
logging.config.dictConfig(log_config)
reset_logging_config()
# Wait for it to connect... # Wait for it to connect...
for handler in logging.getLogger("synapse").handlers: for handler in logging.getLogger("synapse").handlers:
if isinstance(handler, RemoteHandler): if isinstance(handler, RemoteHandler):
@ -107,7 +123,7 @@ async def main(reactor, loops):
else: else:
raise RuntimeError("Improperly configured: no RemoteHandler found.") raise RuntimeError("Improperly configured: no RemoteHandler found.")
await handler._service.whenConnected() await handler._service.whenConnected(failAfterFailures=10)
start = perf_counter() start = perf_counter()

View File

@ -14,14 +14,15 @@
from pyperf import perf_counter from pyperf import perf_counter
from synapse.types import ISynapseReactor
from synapse.util.caches.lrucache import LruCache from synapse.util.caches.lrucache import LruCache
async def main(reactor, loops): async def main(reactor: ISynapseReactor, loops: int) -> float:
""" """
Benchmark `loops` number of insertions into LruCache without eviction. Benchmark `loops` number of insertions into LruCache without eviction.
""" """
cache = LruCache(loops) cache: LruCache[int, bool] = LruCache(loops)
start = perf_counter() start = perf_counter()

View File

@ -14,15 +14,16 @@
from pyperf import perf_counter from pyperf import perf_counter
from synapse.types import ISynapseReactor
from synapse.util.caches.lrucache import LruCache from synapse.util.caches.lrucache import LruCache
async def main(reactor, loops): async def main(reactor: ISynapseReactor, loops: int) -> float:
""" """
Benchmark `loops` number of insertions into LruCache where half of them are Benchmark `loops` number of insertions into LruCache where half of them are
evicted. evicted.
""" """
cache = LruCache(loops // 2) cache: LruCache[int, bool] = LruCache(loops // 2)
start = perf_counter() start = perf_counter()