Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions alphadia/constants/keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ class ConfigKeys(metaclass=ConstantsClass):
REUSE_QUANT="reuse_quant",
REUSE_QUANT_FROM="reuse_quant_from",
FAIL_FAST="fail_fast",
THREAD_COUNT="thread_count",
)

LIBRARY_PREDICTION = ConstantNamespace(
Expand Down
24 changes: 24 additions & 0 deletions alphadia/search_step.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import logging
import os
import sys
from collections.abc import Generator
from copy import deepcopy
from pathlib import Path
Expand Down Expand Up @@ -56,6 +57,10 @@
SPECLIB_FILE_NAME = "speclib.hdf"
SPECLIB_FLAT_FILE_NAME = "speclib_flat.hdf"

# On Windows with Python < 3.13, multiprocessing pools fail with "need at most 63 handles" for more than
# 61 worker processes (cf. concurrent.futures.process._MAX_WINDOWS_WORKERS), fixed in CPython by gh-89240
MAX_WINDOWS_PROCESS_COUNT = 61

# Type stub for extended Logger with progress method
# The progress method is added in reporting.py at module load time
if TYPE_CHECKING:
Expand Down Expand Up @@ -105,6 +110,7 @@ def __init__(
self._config = self._init_config(
config, cli_config, extra_config, self.output_folder
)
self._limit_thread_count(self._config)
self._validate_config()
self._save_config(self.output_folder)

Expand Down Expand Up @@ -191,6 +197,24 @@ def _init_config(

return config

@staticmethod
def _limit_thread_count(config: Config) -> None:
"""Limit the thread count to what multiprocessing supports on the current platform."""
thread_count = config[ConfigKeys.GENERAL][ConfigKeys.GENERAL.THREAD_COUNT]
if (
sys.platform == "win32"
and sys.version_info < (3, 13)
and thread_count > MAX_WINDOWS_PROCESS_COUNT
):
logger.warning(
f"{ConfigKeys.GENERAL}.{ConfigKeys.GENERAL.THREAD_COUNT}={thread_count} exceeds the maximum "
f"of {MAX_WINDOWS_PROCESS_COUNT} supported on Windows with Python < 3.13, using {MAX_WINDOWS_PROCESS_COUNT} instead."
)
config.set_value(
(ConfigKeys.GENERAL, ConfigKeys.GENERAL.THREAD_COUNT),
MAX_WINDOWS_PROCESS_COUNT,
)

@staticmethod
def _load_default_config(file_name="default.yaml") -> Config:
"""Load default config from file."""
Expand Down
1 change: 1 addition & 0 deletions alphadia/workflow/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ def set_value(self, key: str | tuple[str, ...], path: str | list[str]) -> None:
ConfigKeys.LIBRARY_PREDICTION.PEPTDEEP_MODEL_PATH,
),
(ConfigKeys.GENERAL, ConfigKeys.GENERAL.REUSE_QUANT_FROM),
(ConfigKeys.GENERAL, ConfigKeys.GENERAL.THREAD_COUNT),
]:
raise NotImplementedError(
"Only certain values may be set directly, use update() to update the config otherwise."
Expand Down
42 changes: 27 additions & 15 deletions gui/src/renderer/pages/Run.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,30 +108,42 @@ const Output = () => {

const profile = useProfile();

const updateItems = (offset) => {
window.electronAPI.getOutputRowsNew(-1,{limit:100, offset}).then((newItems) => {
backendLengthRef.current += newItems.length;
setItems(items => applyCarriageReturns([...items, ...newItems]));
});
}

React.useEffect(() => {
setItems([]);
backendLengthRef.current = 0;

let isMounted = true;
// prevents overlapping polls from fetching (and appending) the same rows twice
let isFetching = false;

const interval = setInterval(() => {
if (isFetching) {
return;
}
isFetching = true;
window.electronAPI.getOutputLengthNew(-1).then((length) => {
if (isMounted){
if (length > backendLengthRef.current) {
updateItems(backendLengthRef.current);
}
if (length < backendLengthRef.current) {
setItems([]);
backendLengthRef.current = 0;
}
if (!isMounted) {
return;
}
if (length < backendLengthRef.current) {
setItems([]);
backendLengthRef.current = 0;
}
if (length > backendLengthRef.current) {
// fetch all pending rows at once, otherwise the full backlog is replayed
// in small batches when returning to this page during a long run
const offset = backendLengthRef.current;
return window.electronAPI.getOutputRowsNew(-1, {limit: length - offset, offset}).then((newItems) => {
if (isMounted) {
backendLengthRef.current = offset + newItems.length;
setItems(items => applyCarriageReturns([...items, ...newItems]));
}
});
}
}).catch((err) => {
console.error(err);
}).finally(() => {
isFetching = false;
});
}, 100);

Expand Down
25 changes: 25 additions & 0 deletions tests/unit_tests/test_search_step.py
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,31 @@ def test_get_reusable_quant_folders_raises_on_duplicate_raw_file(tmp_path):
step._get_reusable_quant_folders()


@pytest.mark.parametrize(
("platform", "version_info", "thread_count", "expected_thread_count"),
[
("win32", (3, 11), 64, 61),
("win32", (3, 11), 61, 61),
("win32", (3, 13), 64, 64),
("linux", (3, 11), 64, 64),
],
)
def test_limits_thread_count_on_windows(
platform, version_info, thread_count, expected_thread_count, tmp_path, monkeypatch
):
"""Test that `thread_count` is capped on Windows with Python < 3.13 to avoid exceeding the multiprocessing handle limit."""
monkeypatch.setattr(search_step.sys, "platform", platform)
monkeypatch.setattr(search_step.sys, "version_info", version_info)

# when
step = SearchStep(
str(tmp_path / "output"),
config={"general": {"thread_count": thread_count}},
)

assert step.config["general"]["thread_count"] == expected_thread_count


def test_raises_if_reuse_quant_from_is_set_without_reuse_quant(tmp_path):
"""Test that `reuse_quant_from` requires `reuse_quant`."""
with pytest.raises(ConfigError, match="CONFIG_ERROR"):
Expand Down