zapretgui/tests/test_ipsets_effective_entry_check.py
loop-uh e68114e550
All checks were successful
Zapret source guards / validate (push) Successful in 10s
Windows-сборка / Dev Dev 21.1.7.15 опубликован
Отзывчивость: интерфейс в 8 раз меньше ждёт фоновые задачи — пока окно на экране, включён точный системный таймер Windows; боковое меню не читает настройки на каждый пункт, а разбор списков адресов сам уступает интерфейсу
Новый список «занятые фоновые потоки» в jitter-режиме показал, что остальные
рывки первых секунд — ожидание GUI-потоком фоновых задач.

- ui/precise_timer.py. Замер на win10: когда фоновый поток занят счётом,
  GUI-поток возвращает себе GIL за 16,0 мс при обычном системном таймере
  (тик 15,6 мс) и за 2,0 мс при таймере 1 мс. sys.setswitchinterval(0.001)
  сам по себе на Windows не действует: меньше тика система не ждёт. Кадр с
  тремя заходами в Python задерживался на ~48 мс. Точный таймер включается
  при запуске и держится, только пока окно видно: в трее и в свёрнутом виде
  возвращается обычный, чтобы не тратить энергию.
- Боковое меню читало режим запуска из настроек на каждый пункт. Чтение идёт
  под общим замком настроек, который в первую секунду держат фоновые задачи:
  кадр задерживался на 65–100 мс. Теперь режим читается один раз на группу.
- Полный разбор ipset-списка каждые 200 строк отдаёт GIL (time.sleep(0)):
  на 33 тысячах строк интерфейс получал серию рывков по ~50 мс.

Замер рывков на win10 (быстрая машина, пользователь не активен, поэтому
страницы строятся по расписанию): до всех правок 15 рывков, 1514 мс суммарно,
самый длинный 261 мс, первый кадр ~690–870 мс; после — 10–12 рывков,
755–865 мс, самый длинный 166–178 мс, первый кадр 450–540 мс.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 19:59:57 +03:00

106 lines
4.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""«Файл ipset не пустой» отвечается по первой настоящей записи."""
from __future__ import annotations
import sys
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
from lists import ipsets_manager # noqa: E402
class HasEffectiveIpEntryTests(unittest.TestCase):
def _write(self, text: str) -> str:
folder = tempfile.TemporaryDirectory()
self.addCleanup(folder.cleanup)
path = Path(folder.name) / "ipset.txt"
path.write_text(text, encoding="utf-8")
return str(path)
def test_answer_matches_full_parse(self) -> None:
cases = {
"": False,
"# только комментарий\n\n": False,
"не адрес\n1.2.3.4-1.2.3.9\n": False,
"# шапка\n10.0.0.0/8\n": True,
"мусор\n\n8.8.8.8\n": True,
"2001:db8::/32\n": True,
}
for text, expected in cases.items():
with self.subTest(text=text):
path = self._write(text)
self.assertEqual(ipsets_manager._has_effective_ip_entry(path), expected)
self.assertEqual(bool(ipsets_manager._read_effective_ip_entries(path)), expected)
def test_missing_file_has_no_entries(self) -> None:
self.assertFalse(ipsets_manager._has_effective_ip_entry("/nonexistent/zapret/ipset.txt"))
def test_big_file_is_not_parsed_to_the_end(self) -> None:
path = self._write("\n".join(f"10.{i // 256}.{i % 256}.0/24" for i in range(30_000)) + "\n")
real_normalize = ipsets_manager._normalize_ip_entry
calls = 0
def counting(text):
nonlocal calls
calls += 1
return real_normalize(text)
with patch.object(ipsets_manager, "_normalize_ip_entry", side_effect=counting):
self.assertTrue(ipsets_manager._has_effective_ip_entry(path))
# Разбор каждой строки держит GIL: полный проход по 30 тысячам строк
# отнимал время у интерфейса ради ответа «да, не пустой».
self.assertEqual(calls, 1)
def test_rebuild_reports_ready_file_without_full_parse(self) -> None:
path = self._write("10.0.0.0/8\n8.8.8.8\n")
with (
patch.object(ipsets_manager, "rebuild_profile_list_file"),
patch.object(ipsets_manager, "IPSET_ALL_PATH", path),
patch.object(
ipsets_manager,
"_read_effective_ip_entries",
side_effect=AssertionError("полный разбор не нужен"),
),
):
self.assertTrue(ipsets_manager.rebuild_ipset_all_files())
class FullParseYieldsGilTests(unittest.TestCase):
"""Полный разбор списка в фоне регулярно отдаёт GIL интерфейсу."""
def test_long_file_yields_every_block_of_lines(self) -> None:
folder = tempfile.TemporaryDirectory()
self.addCleanup(folder.cleanup)
path = Path(folder.name) / "ipset.txt"
lines = 5 * ipsets_manager._GIL_YIELD_EVERY_LINES + 10
path.write_text("\n".join(f"10.{i // 256}.{i % 256}.0/24" for i in range(lines)) + "\n", encoding="utf-8")
with patch.object(ipsets_manager.time, "sleep") as sleep:
entries = ipsets_manager._read_effective_ip_entries(str(path))
# Windows отбирает GIL у занятого потока только раз в ~15 мс: без
# добровольной паузы разбор 33 тысяч строк давал рывки интерфейса.
self.assertEqual(len(entries), lines)
self.assertEqual(sleep.call_count, 5)
sleep.assert_called_with(0)
def test_short_file_does_not_pause(self) -> None:
folder = tempfile.TemporaryDirectory()
self.addCleanup(folder.cleanup)
path = Path(folder.name) / "ipset.txt"
path.write_text("10.0.0.0/8\n8.8.8.8\n", encoding="utf-8")
with patch.object(ipsets_manager.time, "sleep") as sleep:
self.assertEqual(ipsets_manager._read_effective_ip_entries(str(path)), ["10.0.0.0/8", "8.8.8.8"])
sleep.assert_not_called()
if __name__ == "__main__":
unittest.main()