← Вернуться к статьямОбнаружениеSeptember 19, 202610 мин чтенияCopy article

Kitsune2: Поиск быстрее 251x, путь обновления быстрее 33x, и сплетни начались раньше 15x

В утреннем эксперименте по слою сетевого взаимодействия Holochain: как узлы находят сверстников, загружают отсутствующие данные и возобновляют сплетни, и какие небольшие исправления 6 изменили на пути.

Developed by Robert E. Beckner III (Merlin) | rbeckner.com

Я просыпаюсь около 4:30 a.m. в последнее время, прямо перед рассветом, и наслаждаюсь этим. 19 сентября я использовал это утро, чтобы начать лабораторию по Kitsune2, слою peer-to-peer сетевого взаимодействия под Holochain, и пошёл искать, где он тратит время.К 12:20 p.m. лаборатория произвела 6 патча. Поиск конечных точек: 251× быстрее при 10,000 сохранённых идентификациях. Путь обновления сверстника, который их вызывает: 33× быстрее. Инициирование сплетни после завершённой загрузки: 3,002 мс до 202 мс. Трафик запросов в всплеске: 96.9% меньше сообщений. 3 из изменений исправили работу, которая ждала ненужно, или ждала чего-то, что никогда не произойдёт.Я кодирую уже более 25 лет и строю с AI с GPT-3. Я использую его, чтобы изолировать то, что меня интересует или чего я ещё не понимаю, работать над этим и тестировать то, что, как мы думаем, мы узнали. Мне нравится эта часть: превращать вопрос в эксперимент и видеть, что держится. Это утро, с AI‑кодирующими агентами, я применил этот подход к механизму, который позволяет узлам Holochain держать друг друга в курсе.Приложения Holochain обмениваются данными между участвующими компьютерами, или узлами. Каждый участник хранит свою собственную подписанную историю и помогает проверять и хранить часть общих данных приложения. Это общий магазин – это распределённая таблица хешей, или DHT. Копии находятся у нескольких узлов, поэтому данные могут оставаться доступными, когда их автор отключается. Руководство по DHT Holochain объясняет, как эти обязанности распределяются.Kitsune2 обрабатывает коммуникацию peer-to-peer под этим. Представьте, как узел догоняет: он узнаёт, где находятся peers, сравнивает, что у него есть, с тем, что у них, и запрашивает недостающие части. Gossip – это обмен, через который peers обнаруживают эти различия; fetch получает недостающие операции, единицы данных, которые Kitsune2 передаёт. Принимающий узел передаёт их хранилищу операций своего хоста. Holochain обрабатывает их проверку и интеграцию.
Diagram source
graph TD
    A[Узнайте идентификаторы и конечные точки peers] --> B[Сравните данные через gossip]
    B --> C[Определите недостающие операции]
    C --> D[Получите операции от peers]
    D --> E[Передайте полученные операции в хранилище хоста]
    E --> F[Сообщите, что ожидающие fetch завершены]
    F e1@--> B
    e1@{ animation: fast }
Copy source
Патчи находятся в этой последовательности. Некоторые уменьшают работу узла; другие позволяют ему продолжать, когда работа уже завершена. Указанные выше цифры измеряют эти конкретные шаги на локальных тестовых узлах.

Что именно переместили каждый из изменений 6#

Мы зафиксировали начальную точку на upstream commit 06555ee, исходный код разработки 0.6.0-dev.1. Каждое сравнение использует эту базу плюс записанную последовательность патчей. Метки ниже связывают выводы с нашими записями экспериментов; все патчи 6 теперь отправлены на upstream‑обзор.
МеткаЧто изменилосьИзмеренный результат
K3aВызвать существующего помощника drain при завершении fetchИнициация сплетения после завершённого fetch: 3,002 → 202 мс
K3bБлокировка получателя ограничена dequeueЭффективная параллельность была 1. Настроенные workers теперь действительно перекрываются
K3cПринадлежность к приёму с пометкой генерацииОтменённые вызывающие нити 0 ожидающие ключей. Базовый уровень stranded 8
K3d-P1Очистка ответа на основе наборов1.9–6.1× более быстрая обработка ответа при больших загруженных ожиданиях
K3d-P2Опция пакетирования одних и тех же узлов (по умолчанию 1)Всплески: 87.5–96.9% меньше сообщений запроса
K1Обратный URL к индексу агентаПри 10,000 сохранённых идентификациях: обновления 21–33×, поиски 118–251× быстрее, для 3.10× памяти хранилища
Первый ремонт соединяет завершение получения с задачей gossip, ожидающей его. Изменение вызывает существующую вспомогательную функцию, которая сообщает слушателям, что карта ожидания опустела. Мы протестировали как завершённый fetch, так и fetch, намеренно оставленный открытым.
Gossip initiation after a fetch completes, with the control that must not move (ms)
Chart datamilliseconds
Upstream 06555eeWith the drain notification restored
Fetch completed3001202
Fetch still pending (control)30023001
После завершения gossip может продолжить работу. Пока fetch остаётся в ожидании, обе версии всё ещё достигают таймера fallback. Изолированный тест выше измерил 3,001 → 202 мс; более поздний комбинированный повтор измерил 3,002 → 202 мс. Оба использовали fallback в 3,000 мс и начальную задержку в 200 мс. Около 15× разница фиксирует, какой маршрут выбрала задача: ожидание fallback или пробуждение при завершении.

Каждое изменение находится в одном crate, и 5 из 6 в одном файле#

Crate Rust — это пакет кода. Все патчи 6 находятся в core crate, который содержит peer records и fetch machinery. Они меняют способ, которым gossip получает свои данные и сигналы завершения, без редактирования алгоритма gossip или транспортного уровня сети. Batching добавляет необязательное поле конфигурации core.
МеткаФайлСтроки
K1core/src/factories/core_known_peers.rs+33 / −10
K3acore/src/factories/core_fetch.rs+1
K3bcore/src/factories/core_fetch.rs+11 / −3
K3ccore/src/factories/core_fetch.rs+71 / −53
K3d-P1core/src/factories/core_fetch.rs+6 / −1
K3d-P2core/src/factories/core_fetch.rs, 1 тестовый файл+89 / −17
5 патчей стека в записанном порядке на core_fetch.rs, которые запрашивают отсутствующие операции и обрабатывают ответы. Оставшийся патч находится в core_known_peers.rs, сохраняемая запись того, какая идентификация связана с каким сетевым конечным пунктом. Затем мы применили все 6 к одному дереву исходного кода и запустили их вместе.

Данные пришли, но gossip всё ещё ждёт#

CoreFetch хранит карту операций, которые он запросил и от кого. Когда ответ партнёра приходит и операции успешно сохраняются, код удаляет эти идентификаторы из карты. Другие части файла, путь send-error и путь очистки партнёра, вызывают вспомогательную функцию под названием notify_listeners_if_queue_drained сразу после удаления записей. Путь успеха его никогда не вызывал.Узел может расширить часть DHT, которую он хранит, называемую его storage arc. Чтобы сделать это, ему нужно наверстать данные в более широком диапазоне. В пути роста арка мы тестировали, gossip ждёт завершения незавершённых fetches, прежде чем инициировать с партнёром, с таймером как запасным вариантом. Данные пришли и карта ожидания опустела, но задача, ожидающая этот сигнал, оставалась спящей до истечения таймера.
Diagram source
sequenceDiagram
    participant G as Gossip arc-growth loop
    participant F as CoreFetch
    participant P as Peer
    G->>F: Register a drain waiter
    P->>F: Final response, operations stored
    F->>F: retain() empties the pending map
    Note over F: helper exists, never called
    F--xG: Notification that never arrives
    G->>G: Initiate after the roughly 3-second fallback
Copy source
Проверка после завершения скрывает разницу: новый зарегистрированный waiter видит пустую карту и сразу возвращается. Мы зарегистрировали наш до завершения. Как только данные были сохранены, тот же официант всё ещё не получил уведомление по исходному коду; с патчем он получил одно. Это исправляет передачу между получением данных и началом следующего обмена сплетнями.Мы также проверили частичные ответы, ошибки хранилища, несколько слушателей и слушателей, регистрирующихся как завершённые запросы или новые работы, пришедшие. Частичный ответ должен оставить официанта в ожидании; только завершение последнего незавершённого запроса должно его разбудить.

5 workers были настроены, а 1 действительно отправлял#

Исходящая сторона CoreFetch запускает настроенное число задач‑работников, вытягивающих из одного общего канала. Цикл был написан так:rustCopy source
while let Some(..) = outgoing_request_rx.lock().await.recv().await {    // send the request}
Блокировка позволяет 1 worker взять следующий запрос из общей очереди. Но Rust удерживает эту временную защиту блокировки в течение тела этого while let, включая ожидание сетевой отправки. Пока 1 worker отправлял, другой 4 даже не мог собрать свой следующий запрос. Все запросы всё равно могли в конечном итоге прийти, поэтому тест доставки мог пройти, пока настроенная параллельность не использовалась.Мы переместили получение в короткий блок, чтобы блокировка была освобождена до начала отправки. Работники всё ещё по очереди собирают запросы, а затем отправляют их параллельно. Медленная отправка больше не удерживает блокировку очереди, пока другие работники не подготовили запросы.
Outgoing sends in flight at the same moment, by configured worker count
Chart dataconcurrent sends observed
Upstream 06555eeDequeue-scoped guard
1 worker11
2 workers12
5 workers15
Измерение держит каждую отправку открытой с приостановленным таймером и считает, сколько вошли, прежде чем любой будет освобожден, затем подтверждает, что ничего не входит за пределы настроенного предела.

Отмена вызывающего оставила узел в ожидании неотправленных операций 8#

Запрос fetch проходит через 2 записи: карта ожидания указывает, чего ожидает узел, а исходящая очередь содержит работу для отправляющих работников. request_ops, метод, который запрашивает операции, добавил целую партию в карту ожидания до того, как дождался места в очереди.Если вызывающий был отменён во время ожидания места, некоторые операции уже были помечены как ожидающие, хотя ни один работник их не получил. Мы заполнили очередь, отменили заблокированного вызывающего и позволили всем 16,385 принятым запросам завершиться. 8 операции остались в ожидании. Они никогда не попали в исходящую очередь, поэтому ответа, очищающего их, не было.Это напрямую связано с первой поправкой: уведомление о завершении не может сработать, пока карта всё ещё содержит незавершённую работу. Здесь собственный учёт узла удерживал его в ожидании.Патч сначала резервирует место в очереди, затем записывает и enqueues операцию без дополнительного await между этими шагами. Каждый элемент также содержит тег поколения, поэтому очистка от более старой неудачной отправки не может стереть более новую заявку на ту же операцию. Работа, уже принятая, переживает отмену вызывающего; работа, всё ещё ожидающая принятия, не оставляет за собой записи в ожидании.Это восстанавливает владение запросами. Общие ограничения памяти остаются отдельным вопросом дизайна, поскольку вызывающий всё ещё может предоставить большую партию.

Меньше сообщений запроса, с ограничением на то, насколько каждый может расти#

Узел, который догоняет, может потребовать много операций от одного и того же партнера. Формат запроса Kitsune2 уже поддерживает список идентификаторов, но исходящий код отправляет 1 на сообщение. Перед объединением этих запросов мы проверили, как будут обрабатываться более крупные ответы.Для каждой ожидающей операции очистка ответа сканировала список возвращенных идентификаторов, чтобы увидеть, можно ли удалить эту запись. С 8,192 ожидающими записями и 1,024 возвращенными идентификаторами, это повторяет большое количество поисков. Создание HashSet возвращенных идентификаторов делает каждую проверку членства дешевой; обработка ответа измерила 6.1× быстрее в этом случае. При наличии только 16 возвращенных идентификаторов хранение данных доминировало, и изменение принесло мало.Затем мы объединили готовые запросы для одного и того же партнера. В всплеске 64 небольших операций размеры партий 8 и 32 сократили сообщения запроса на 87.5% и 96.9% соответственно.
Burst behaviour relative to the default batch size of 1 (percent of control)
Chart datapercent of control
Request messagesElapsed time
Batch 1 (control)100105.4
Batch 812.572.6
Batch 323.167.7
Всплеск завершился примерно 27–32% раньше. Те же операции всё ещё нужно было передать и сохранить; экономия была в том, сколько запросов их переносили. Контроль batch-1 измерил 105.4% базового прошедшего времени, показывая примерно 5% дрейф времени даже при неактивном пакетировании.Рабочий объединяет только запросы, которые уже готовы, поэтому одинокий запрос никогда не ждёт заполнения партии. Разделение партий использует как настроенное ограничение идентификаторов, так и 4,096-байтовый закодированный порог запроса. Подсчёт предметов сам по себе не ограничивает их общий размер; тест с 200 идентификаторами разбивает их на несколько ограниченных сообщений. Один идентификатор всё равно отправляется, если он сам превышает этот порог, поэтому это не универсальная граница размера провода. Ответы несут фактические данные и потребовали бы отдельного ограничения размера на отвечающем узле.Кандидат сохраняет размер пакета 1 как значение по умолчанию и принимает существующие конфигурации без изменений. Мы оставили возможность выбора больших пакетов, потому что отдельный тест для одиночного запроса дал непоследовательные результаты по хвостовой задержке, описанные ниже.

Удаление сканирования из пути обновления стоило 33×, и 251× на самом поиске#

CoreKnownPeers запоминает идентичности и сетевые конечные точки, связанные с ними. Несколько идентичностей могут делить конечную точку, а заблокированная идентичность всё равно должна быть идентифицируемой после того, как она была удалена из активного хранилища узлов. 10,000 сохранённые идентичности, следовательно, означают историю идентичностей в этом хранилище, а не 10,000 компьютеров, подключённых одновременно.При обновлении хранилища узлов контроль доступа спрашивает, какие идентичности принадлежат конечной точке. Поиск, get_by_url, сканировал всю сохранённую карту под блокировкой каждый раз. По мере роста этой истории обновление текущей информации о узле становилось дороже.Мы добавили индекс от конечной точки URL к связанным с ней идентичностям: HashMap<Url, HashSet<AgentId>>`. Оригинальные записи остаются авторитетными. Когда идентичность меняет конечную точку, индекс перемещает её в соответствующий набор; поиск затем может перейти напрямую к этому набору. Оба варианта прошли проверки 19,422 результата, охватывающие обновления, устаревшие объявления, удаления и общие конечные точки.
Microseconds per 16-advertisement update call (log scale)
Chart datamicroseconds per update call
retained identitiesUpstream linear scanDerived URL index
1006.8574.911
1,00018.1034.774
10,000154.8494.72
Каждый отложенный вызов выше вставляет 16 объявления и запускает слушатели контроля доступа. При 10,000 сохранённых идентичностях это снизилось с примерно 155 µs до 4.72 µs, прирост 33× в паре. Индексированная версия остаётся почти плоской при этих размерах, потому что эти слушатели больше не сканируют растущую историю.Измерено на API вместо через вызывающий, поиск сам переходит от сканирования к хешу. Пропуск при 10,000 сохранённых идентичностях снизился с 11,423 ns до 45 ns, пара 251×, а один попадание с 9,309 ns до 79 ns, 118×. Запрос, который возвращает много идентичностей, всё равно масштабируется по тому, что он возвращает, поэтому случай shared-URL получает 4.4× вместо двух порядков величины. 251× — это изолированный результат поиска. 33× измеряет более крупный вызов обновления, который его использует; эти приросты нельзя добавить или умножить.
Requested store memory at 10,000 retained identities, by endpoint shape
Chart datamegabytes
UpstreamWith the index
Mixed endpoints1.23.71
All shared1.21.74
All URL-less1.21.2
3.10× на смешанном хранилище, дополнительный 2.51 МБ на 10,000 идентичности. Когда каждая идентичность использует один и тот же конечный пункт, получаемая карта небольшая, а стоимость падает до 1.45×. Когда у идентичности нет URL, ничего не выводится, и стоимость нулевая. Эти числа – байты, запрошенные аллокатором для сохраненного хранилища, а не общая память узла.

С учётом всех 6 патчей узел мог завершить и продолжить#

Объединённый повтор использовал реальные компоненты gossip, fetch, peer-access и memory-store Kitsune2, общаясь через его транспорт в памяти для тестов. Мы выполнили последовательность из 5 шагов на оригинальных и патченных деревьях исходного кода:
  1. Обновить информацию о пире. Идентификатор меняет конечную точку. Устаревшая информация, приходящая позже, оставляет более новую запись нетронутой, и заблокированный идентификатор остаётся разрешимым для контроля доступа. Оба варианта проходят; индекс сохраняет существующие решения.
  2. Начать gossip после завершения fetch. Патченный узел инициирует после 202 мс. Оригинал берёт резервный путь при 3,002 мс.
  3. Получить данные в неправильном порядке. Запрошенные операции 3 приходят как ответы 4, включая дубликат. Оба варианта сохраняют их последовательно и связывают количество байт с правильной операцией.
  4. Завершить fetch. Сохранённые операции можно извлечь. Патченный узел также пробуждает слушателя, который зарегистрировался, пока эти операции ожидали.
  5. Продолжить после неудачного пира. Контролируемое неудачное отправление очищает состояние запроса того пира. Другой пир предоставляет операцию, карта ожидания опустошается, и последующий fetch проходит успешно. В оригинале проверки уведомления о завершении не проходят.
Отдельные тесты взаимодействия объединили отмену с одновременными отправками и комбинированную пакетизацию с уведомлением о завершении. Ремонты держались вместе: принятая работа завершена, отменённые приёмы не оставили сиротских записей, и слушатели пробуждались после окончательного ответа.Это доводит нас до того, что данные получены в хранилище хоста, и следующий шаг сетевого взаимодействия может продолжиться. Валидация и интеграция Holochain всё ещё следуют за пределами этой лаборатории. Мы ещё не измерили, насколько эти изменения сокращают полное время синхронизации реального узла.

Что стоит за каждой выгодой#

Индекс занимает память и делает изменения конечных точек более дорогими. В оригинальном тесте с небольшим хранилищем обновление фиксированного набора идентичностей на новые URL выполнялось со скоростью 0.946× базовой скорости с 100 сохранёнными идентичностями. При 10,000 изолированная запись, меняющая конечную точку, обошлась в 3.68× так же много. Поддержание дополнительного индекса стоит денег; экономия на поиске должна окупить его.Поскольку этот компромисс реальный, мы искали рабочую нагрузку, где индекс теряет. 36 конфигурации были заранее объявлены до измерения, 432 парные запуски выполнены, и транскрипты результатов зашифрованы идентично между вариантами.
Paired speedup across the 36-config workload envelope, by mix (log scale)
Chart datapaired speedup
retained identitiesSteady, no rebind100% rebindShared endpointsURL-less heavy
1001.611.471.421.09
1,0007.26.264.122.13
10,00055.5636.425.5512.16

Reference line, parity: 1

Каждая нагрузка в этой более широкой матрице имела медиану выше паритета, включая смесь, где каждый принятый обновление менял конечную точку: 1.47× при 100 идентичностях и 36.42× при 10,000. Эти обновления также инициируют поиски контроля доступа, и их экономия перевешивала дополнительные записи. Эта более широкая нагрузка отличается от раннего теста с фиксированным набором; её положительный результат оставляет регрессию небольшого хранилища в записи.Случай с 100 идентичностями, в основном без URL, остаётся неопределённым: его медиана 1.09× включала пару результатов ниже паритета. Нам всё ещё нужны трассировки реальных узлов, показывающие количество сохранённых идентичностей, изменения конечных точек и частоту поиска, прежде чем рекомендовать индекс для конкретной нагрузки.Параллельные отправки выявили взаимодействие очистки. Как только работники могли перекрываться, очистка неудачной отправки могла выполняться, пока другой запрос к тому же пиру находился в пути. Это сделало существующую очистку по всему пиру шире, чем работа, которая не удалась. Патч владения сузил удаление до записей, принадлежащих той же генерации запроса. Тестирование патчей вместе имело значение здесь.Задержка однократного запроса при пакетировании остаётся неопределённой. Мы измеряли p95, время, в течение которого 95% запросов завершаются. Для запроса, отправленного в одиночку, окончательный запуск работника показал 1.01–1.08× базовую p95; независимый повтор показал 1.44–1.58×. Эти фикстуры занимают десятки микросекунд, и их управления также дрейфовали. Улучшение всплеска воспроизводилось, но результат одиночного запроса не стабилизировался, поэтому более крупные партии остаются опциональными.

Повторные сравнения и зависшие отправки остаются открытыми#

Мы также изучили 2 другие части той же последовательности синхронизации. Ни одна из них не привела к принятым патчам.Сравнение того, что уже держат peers (K4). Gossip использует компактные резюме хранимых данных для поиска различий. В крупнейшем тесте создание и сравнение неизменённого снимка материализовало 262,144 хэши; повторение этого 10 раз давало одинаковый вывод каждый раз. Повторное использование предыдущего резюме выглядит привлекательно, но хост‑хранилище может измениться без надёжного сигнала для инвалидирования сохранённого результата. Кэшированное резюме тогда могло скрыть различие между peers. Альтернативы, которые мы оценили, также не достигли требуемой 20% экономии времени на хранилище в памяти. Измерения из реального Holochain store являются следующим недостающим элементом.Восстановление при зависании gossip‑отправки (K2). Чтение источника вызвало вопрос о том, удерживает ли отправка блокировку, которую также требуется таймаут‑задаче. Нам также нужно было установить, что происходит с базовым транспортным потоком Iroh, если эта отправка отменяется наполовину. Провайдер агента заблокировал работу над драйвером воспроизведения, поэтому тест среды выполнения так и не завершился. Это остаётся неиспытанной проблемой, без патча или результата надёжности.

Что мы измерили, и что остаётся измерить#

Эти эксперименты проводились на исходном коде 0.6.0-dev.1, на 1 общей Apple M4 Max с 16 логическими ядрами. Мы использовали локальные тестовые узлы и транспорт в памяти, без закрепления ядер и второго физического хоста. Результаты охватывают названные компоненты и их взаимодействия. Пропускная способность сети, сходимость всей сети, общее объём памяти узла и задержка приложения Holochain остаются неизмеренными.Метод следует моей работе по benchmark-driven development. Мы записывали каждый вопрос и порог принятия, держали исходный код и фикстуры фиксированными в каждом сравнении, и сохраняли команды, образцы и ошибки в приписываемых только к приёмке квитанциях.Для 3 исправлений те же тесты не прошли на оригинальном коде при ожидаемых утверждениях и прошли с патчем. Для оптимизаций обе версии должны были сохранять поведение, затем измерения должны были соответствовать заявленному порогу. Запуски времени чередовали порядок и держали компиляцию вне измеряемого окна. Надзорный агент повторно запускал команды принятия на замороженном исходном коде перед принятием каждого результата; разногласия по задержке одиночного запроса остались в окончательном отчёте.Завершённый пакет содержит 204 каталоги квитанций. Объединённые тесты были приняты в 12:17 p.m., и результаты терминала были зафиксированы в 12:20 p.m. Финальная проверка закрытия была проведена в 2:38 p.m. Все 6 патчей теперь открыты для upstream-обзора: completion notification, worker concurrency, admission ownership, response cleanup, request batching, и endpoint indexing.

Ключевые атрибуции#

От меня. Сам лабораторный процесс: мой benchmark-driven метод, направленный на Kitsune2 чтобы выяснить, где он тратит время.От GPT-6 Astra. Исправления уведомления о завершении и владения приёмкой: диагностика, патч и тесты, которые падают на исходном коде. Он также уточнил эту статью из лабораторных записей.От GLM-5.3. Исправление параллелизма рабочих, очистка ответов и пакетирование, матрица нагрузки конфигурации 36 для индекса, профилирование DHT за открытым вопросом K4, и объединённое воспроизведение всех 6 патчей.От AI. Индекс конечных точек и его измерения, надзорный агент, который повторно запускал каждое принятие на замороженном исходном коде до того, как результат засчитан, и первый черновик этой статьи.Критики, которые остались.Достигнуто вместе. Последовательность догоняния, которой следует статья: узел узнаёт своих партнёров, сравнивает, что у него есть, получает, чего не хватает, сохраняет и продолжает, при этом каждый патч размещён на шаге, который он меняет.Основано на. Заменяемые фабрики компонентов и транспорт в памяти основной команды Holochain, которые позволяют лаборатории тестировать эти поведения в реальных компонентах Kitsune2.#benchmarking#distributed-systems#rust#performance#AI#agents#open-source#experiential#insights—Robert E. Beckner III (Merlin)Продолжить чтениеПредыдущийИнтеллект не может подчиняться: Оценка, Авторитет и Что Показывает ОтказСледующийОшибки кэширования запросов могут обойтись вашей организации почти в 6 раз больше за каждый ход ИИQwen 3.8-27B на M1 Ultra: от 12.1 до 20.3 ток/с измеряя всёОбнаружениеAugust 14, 20268 мин чтенияMTPLX на M1 Ultra: Те 25 ток/с Настройка, которая обслуживала 16.5ОбнаружениеAugust 15, 20267 мин чтенияПоследний CLI: Восстановление Базы 51, На Которой Были Построены Мои ИнструментыОбнаружениеAugust 19, 202610 мин чтенияСвязанные проекты175-страница Китайская рукописьИнновации ИИAIA - советник по ИИИнновации в ИИ