ci(portability): keep Windows test files on the work disk (#487)
The Windows runner's TEMP is on C:, where a flush took about 6 ms and a test database's create-to-delete about 111 ms; on D: the same took 0.5 ms and 23 ms. The job now points TEMP/TMP at RUNNER_TEMP and lists the 40 slowest tests. The fake HTTPS handlers get a TLS context that reads no certificate store, and the missing-share tests name localhost instead of an unknown server. The temporary diagnostics are removed.
This commit is contained in:
@@ -51,22 +51,19 @@ jobs:
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements-dev.txt
|
||||
|
||||
- name: DIAG 487 microbench (temporary)
|
||||
if: matrix.python-version == '3.12'
|
||||
run: python diag_487.py
|
||||
|
||||
- name: DIAG 487 profile (temporary)
|
||||
if: matrix.python-version == '3.12'
|
||||
run: python diag_487_profile.py tests.test_database tests.test_review_schema tests.test_api_admin tests.test_lora_utils
|
||||
# The runner's TEMP is on C:, the OS disk, where a flush took about 6 ms
|
||||
# and a test database's create-to-delete about 111 ms; on the D: work
|
||||
# disk the same took 0.5 ms and 23 ms (macOS: 19 ms). Tests write their
|
||||
# databases under TEMP, so it moves to the runner's temp folder on D:
|
||||
# (measured in #487, run 40915866444).
|
||||
- name: Keep test files on the work disk (Windows)
|
||||
if: runner.os == 'Windows'
|
||||
shell: pwsh
|
||||
run: |
|
||||
"TEMP=$env:RUNNER_TEMP" >> $env:GITHUB_ENV
|
||||
"TMP=$env:RUNNER_TEMP" >> $env:GITHUB_ENV
|
||||
|
||||
- name: Run tests
|
||||
env:
|
||||
PM_TEST_SLOWEST: "40"
|
||||
run: python -m tests.run_suite -v
|
||||
|
||||
- name: DIAG 487 microbench without Defender realtime (temporary)
|
||||
if: always() && matrix.python-version == '3.12' && runner.os == 'Windows'
|
||||
shell: pwsh
|
||||
run: |
|
||||
Set-MpPreference -DisableRealtimeMonitoring $true
|
||||
python diag_487.py
|
||||
|
||||
+11
@@ -251,6 +251,17 @@ the process exits non-zero.
|
||||
stacks.
|
||||
- When the watchdog fires under coverage, that run's coverage data is lost; the
|
||||
job fails anyway and the dump is what matters.
|
||||
- `PM_TEST_SLOWEST=N` (off by default) lists the N slowest tests after the
|
||||
summary, each timed from its `start:` line to the next one like the hang
|
||||
limit. The Windows and macOS jobs (`portability.yml`) set it to 40, so every
|
||||
run shows where its time went (#487).
|
||||
- On the Windows runners `TEMP` sits on the OS disk C:, where a flush took about
|
||||
6 ms and a test database's create-to-delete about 111 ms (D: 0.5 ms and 23 ms,
|
||||
macOS 19 ms). `portability.yml` points `TEMP`/`TMP` at `RUNNER_TEMP` on D:
|
||||
before the suite. A test that waits on a fixed busy timeout, a network name
|
||||
(`\\server\share`: use `\\localhost\<missing share>`) or a default TLS
|
||||
context (it reads the Windows certificate store) costs seconds there; the
|
||||
slowest list shows it.
|
||||
|
||||
Every job in `test.yml`, `code-quality.yml` and `viewerjs-check.yml` has
|
||||
`timeout-minutes` (`tests/vendor/ci-workflow.test.mjs` checks it). The rule is
|
||||
|
||||
-108
@@ -1,108 +0,0 @@
|
||||
"""Temporary #487 diagnostics (removed before merge)."""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import sqlite3
|
||||
import statistics
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
logging.disable(logging.CRITICAL)
|
||||
sys.path.insert(0, os.getcwd())
|
||||
from database import models # noqa: E402
|
||||
from database.models import PromptModel # noqa: E402
|
||||
from database.operations import PromptDatabase # noqa: E402
|
||||
|
||||
|
||||
def bench(label, fn, n):
|
||||
times = []
|
||||
for i in range(n):
|
||||
t = time.perf_counter()
|
||||
fn(i)
|
||||
times.append((time.perf_counter() - t) * 1000)
|
||||
print(
|
||||
f" {label:44s} median {statistics.median(times):8.2f} ms"
|
||||
f" mean {statistics.mean(times):8.2f} ms"
|
||||
)
|
||||
|
||||
|
||||
def run(base):
|
||||
d = tempfile.mkdtemp(dir=base)
|
||||
print(f"== base {base}")
|
||||
|
||||
def small(i):
|
||||
p = os.path.join(d, f"f{i}.txt")
|
||||
with open(p, "wb") as h:
|
||||
h.write(b"x" * 4096)
|
||||
os.unlink(p)
|
||||
|
||||
def fsync(i):
|
||||
p = os.path.join(d, f"s{i}.txt")
|
||||
with open(p, "wb") as h:
|
||||
h.write(b"x" * 4096)
|
||||
h.flush()
|
||||
os.fsync(h.fileno())
|
||||
os.unlink(p)
|
||||
|
||||
def raw(sync):
|
||||
def f(i):
|
||||
p = os.path.join(d, f"r{sync}{i}.db")
|
||||
c = sqlite3.connect(p)
|
||||
c.execute("PRAGMA journal_mode=WAL")
|
||||
c.execute(f"PRAGMA synchronous={sync}")
|
||||
c.execute("CREATE TABLE t(x)")
|
||||
c.execute("INSERT INTO t VALUES(1)")
|
||||
c.commit()
|
||||
c.close()
|
||||
for s in ("", "-wal", "-shm"):
|
||||
if os.path.exists(p + s):
|
||||
os.unlink(p + s)
|
||||
|
||||
return f
|
||||
|
||||
def pdb(i):
|
||||
p = os.path.join(d, f"p{i}.db")
|
||||
db = PromptDatabase(p)
|
||||
db.save_prompt(
|
||||
text=f"t{i}",
|
||||
category=None,
|
||||
tags=[],
|
||||
rating=None,
|
||||
notes=None,
|
||||
prompt_hash=str(i),
|
||||
)
|
||||
PromptModel.close_all_instances()
|
||||
for s in ("", "-wal", "-shm"):
|
||||
if os.path.exists(p + s):
|
||||
os.unlink(p + s)
|
||||
|
||||
def mkrm(i):
|
||||
t = tempfile.mkdtemp(dir=d)
|
||||
open(os.path.join(t, "a"), "w").close()
|
||||
shutil.rmtree(t)
|
||||
|
||||
bench("create+write+close+unlink 4k file", small, 100)
|
||||
bench("same + fsync", fsync, 50)
|
||||
bench("mkdtemp + file + rmtree", mkrm, 100)
|
||||
bench("raw sqlite WAL db NORMAL", raw("NORMAL"), 30)
|
||||
bench("raw sqlite WAL db OFF", raw("OFF"), 30)
|
||||
bench("PromptDatabase create+save+close+unlink", pdb, 20)
|
||||
orig = models.configure_connection
|
||||
|
||||
def off(conn):
|
||||
orig(conn)
|
||||
conn.execute("PRAGMA synchronous = OFF")
|
||||
|
||||
models.configure_connection = off
|
||||
bench(" same with synchronous=OFF", lambda i: pdb(1000 + i), 20)
|
||||
models.configure_connection = orig
|
||||
shutil.rmtree(d, ignore_errors=True)
|
||||
|
||||
|
||||
bases = [tempfile.gettempdir()]
|
||||
if os.environ.get("RUNNER_TEMP"):
|
||||
bases.append(os.environ["RUNNER_TEMP"])
|
||||
for b in bases:
|
||||
run(b)
|
||||
@@ -1,18 +0,0 @@
|
||||
"""Temporary #487 profile of a few modules (removed before merge)."""
|
||||
|
||||
import cProfile
|
||||
import logging
|
||||
import pstats
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
logging.disable(logging.CRITICAL)
|
||||
names = sys.argv[1:]
|
||||
suite = unittest.defaultTestLoader.loadTestsFromNames(names)
|
||||
pr = cProfile.Profile()
|
||||
pr.enable()
|
||||
unittest.TextTestRunner(stream=open(__import__("os").devnull, "w")).run(suite)
|
||||
pr.disable()
|
||||
st = pstats.Stats(pr)
|
||||
st.sort_stats("tottime").print_stats(30)
|
||||
st.sort_stats("cumtime").print_stats(60)
|
||||
@@ -45,7 +45,9 @@ SEEDED_PATHS = (
|
||||
"/srv/someone/renders",
|
||||
"C:\\Users\\someone\\Documents\\renders",
|
||||
"D:/Users/someone/prompts.db",
|
||||
"\\\\nas\\share\\someone\\renders",
|
||||
# A share on this machine: on Windows the report resolves every source,
|
||||
# and an unknown server name made that wait about 2.9 s (#487).
|
||||
"\\\\localhost\\pm-no-such-share\\someone\\renders",
|
||||
)
|
||||
_WINDOWS_ABSOLUTE = re.compile(r"^[A-Za-z]:[\\/]")
|
||||
TOP_KEYS = {
|
||||
|
||||
@@ -844,8 +844,12 @@ class FolderGuardTests(unittest.TestCase):
|
||||
|
||||
def test_missing_folders_add_nothing(self):
|
||||
self.assertIsNone(layout_module._guard_folder(str(self.tmp / "x"), "/home"))
|
||||
# A missing share on this machine: an unknown server name made
|
||||
# Windows wait about 2.8 s for the network (#487).
|
||||
self.assertIsNone(
|
||||
layout_module._guard_folder("\\\\server\\share\\loras", str(self.tmp))
|
||||
layout_module._guard_folder(
|
||||
"\\\\localhost\\pm-no-such-share\\loras", str(self.tmp)
|
||||
)
|
||||
)
|
||||
self.assertEqual(
|
||||
layout_module._guard_folder(str(self.tmp), "/nonexistent-home"),
|
||||
|
||||
@@ -9,6 +9,7 @@ import email
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import ssl
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
@@ -669,6 +670,15 @@ class TestStaleDownloadSweep(unittest.TestCase):
|
||||
self.assertTrue(old.exists())
|
||||
|
||||
|
||||
def _unused_tls_context():
|
||||
"""A TLS context for the fake handlers, which never open a connection.
|
||||
|
||||
HTTPSHandler() builds a default context, and on Windows that reads the
|
||||
system certificate store: about 0.14 s per handler (#487).
|
||||
"""
|
||||
return ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
||||
|
||||
|
||||
class _FakeHTTPSHandler(urllib.request.HTTPSHandler):
|
||||
"""Serves canned responses through a real urllib opener, recording requests.
|
||||
|
||||
@@ -677,7 +687,7 @@ class _FakeHTTPSHandler(urllib.request.HTTPSHandler):
|
||||
"""
|
||||
|
||||
def __init__(self, redirect_to):
|
||||
super().__init__()
|
||||
super().__init__(context=_unused_tls_context())
|
||||
self.redirect_to = redirect_to
|
||||
self.requests = []
|
||||
|
||||
@@ -745,7 +755,7 @@ class _ChainHTTPSHandler(urllib.request.HTTPSHandler):
|
||||
urllib opener, recording every request; an unknown URL answers 404."""
|
||||
|
||||
def __init__(self, routes):
|
||||
super().__init__()
|
||||
super().__init__(context=_unused_tls_context())
|
||||
self.routes = routes
|
||||
self.requests = []
|
||||
|
||||
|
||||
Reference in New Issue
Block a user