fix(mcp): תקרת שורות ותקרת טוקנים בפרסר ה-Markdown, ומגבלת קצב של 45 (#3391)
✨ תיאור קצר
- עד עכשיו התקרה היחידה בתוך
md_parserספרה כותרות, וקובץ עוין בלי אף כותרת עבר אותה בלי סירוב: 500KB של שורות-תבליט עלו כ-142.5MiB לפרסור אחד, וקובץ של 128KB עם הרבה טבלאות רחבות הגיע ל-3GB ונפל ב-MemoryError(התקרה של upstream לתאים משלימים, #364, היא לכל טבלה). - ה-PR מוסיף שתי תקרות בתוך הפרסר: 8,000 שורות שנבדקות לפני הפרסור, ו-45,000 טוקנים שנבדקים ברגע שנוצר כל טוקן — גם באמצע טבלה ובאמצע רשימה. עם שתיהן, הקלט העוין הגרוע שנמצא נכנס בהקצאה לחוט — החסם העליון שלו 95.9% ממנה — ואף קובץ אמיתי אינו נחסם. מגבלת הקצב יורדת מ-60 ל-45 בדקה, כדי ש"זהות אחת לא עוברת דקת מעבד בדקה" תהיה טענה שנבדקת.
- ובסבב 2 תוקנה שיטת המדידה עצמה: מאז #3429 הסקריפט הסתיר עד כחצי MB מהשיא של כל פרסור, תמיד לכיוון "נכנס". עכשיו השיא נמדד מאיפוס של
VmHWM, על 40 סידורי זיכרון, ונשפט כחסם עליון. כל מספר שנמדד בשיטה הישנה ומופיע בתיעוד נמדד מחדש ועודכן כאן. הקבוע 72 נשאר, ומחזיק בזכות התקרות של ה-PR הזה, במרווח של 4.1% (פירוט בסעיף "סבב סקירה 2"). - ובסבב 3: קלטי המעבד נמדדים עכשיו בזיכרון על כל 40 הסידורים, ופסק הדין בודק שכל מדידה רצה על כולם — ולא רק המדידה המכריעה. ויש ממצא פתוח על זמן המעבד שדורש החלטה שלך (בסוף הסעיף "סבב סקירה 3").
📦 שינויים עיקריים
- קוד (Backend)
- בוט טלגרם
- מסד נתונים/מיגרציות
- תיעוד (docs/)
- DevOps/CI/CD
פירוט נקודות:
-
services/doc_sections.py— שתי חריגות סירוב חדשות:TooManyLines(total_lines, limit)ו-TooManyTokens(line, limit), לידInconsistentLineEndingsובאותו נימוק (חוזה הסירוב המשותף; המטפל מייבא משם). -
services/md_parser.py—MAX_LINES = 8_000,MAX_TOKENS = 45_000(עם המדידות ליד כל אחד), ו-WORST_CASE_CPU_SECONDS = 0.66. תקרת השורות רצה אחרי שער הכניסה ולפני כל העתקה, פיצול או פרסור, ביחידה שלcount_lines. תקרת הטוקנים יושבת ב-appendשל רשימת הטוקנים עצמה (_CappedTokens), שכלל core (ck_max_tokens) מתקין לפניblock— לא בבנאי שלStateCore, ששם רשימה ריקה מוחלפת בשקט (tokens or []). מעקה אחרי הפרסור בודק שהרשימה שחזרה היא זו שהותקנה, ומעקה בכלל עצמו בודק שלא נוצרו טוקנים לפניו — שניהםRuntimeError. המצב של כל פרסור יושב ב-env(U1), והחימום נשאר הצעד האחרון (K15).Noneמכבה כל תקרה;token_countו-front_matter_endנשארים בלי תקרות, בכוונה (סקריפטים על קבצי הריפו). -
mcp_server/docs_handlers.py—too_many_lines(עםmaxו-total_lines, בליline) ו-too_many_tokens(עםmaxו-line, בליlineכשעוד אין מיקום).maxהוא התקרה שהחריגה נבנתה איתה. -
mcp_server/limits.py—DEFAULT_RATE_LIMIT_PER_MINUTE = 45, והנימוק בשמות (לא במספרים). גםservices/config_inspector_service.pyו-docs/environment-variables.rst. -
scripts/measure_md_parse_cost.py— מודד עכשיו גם את הקלט העוין כמו שהכלי מקבל אותו: 30 צורות עוינות (עשר הצורות, טבלאות מעל תקרת התאים, הגדרות קישור, ציטוט מקונן,\r\nעם תו אסטרלי ועוד), הקלט המשולב שהוא הגרוע בזיכרון, והקלטים הגרועים במעבד (על כל 40 הסידורים; זמן המעבד שלהם נשפט על 5 הריצות הראשונות) — כל אחד בתהליך נקי, עםRLIMIT_ASוזמן מעבד. שורת פסק דין, וקוד יציאה 1 כשמדידה כמו הכלי לא מדדה פרסור (MEASURED_OUTCOMES: לא סירוב לפני הפרסור ולאMemoryError), כשהזיכרון עובר את_PARSE_COST_BYTES, או כשהמעבד עובר אתWORST_CASE_CPU_SECONDS.--doubling— בדיקת ההכפלה (30 צורות × 3 גדלים, בלי תקרות) שנשארת בריפו; משאב שמתחת לרצפת הרעש שלו (MEMORY_NOISE_FLOOR_BYTES,CPU_NOISE_FLOOR_SECONDS) אינו נבדק לגדילה. -
scripts/md_parser_upgrade_zero_diff.py—_REFUSALSכולל את שתי החריגות החדשות, ומקרה הסירובmd_too_many_sectionsהוחלף ב-md_too_many_linesו-md_too_many_tokens. -
tests/test_md_parser_oracle.py— האורקל משווה בלי התקרות (החלטה 2), ובשורה 3 של_KNOWN_DIVERGENCESעצמה (שדהunreachableחדש) כתוב שהפער אינו נגיש דרך הכלי כי הוא מתחיל רק אחרי 196,608 טוקנים. טסט גוזר את המספר (שלושה טוקנים לכל תא משלים × התקרה) ובודק שהכלי מסרב לכל 8 הצורות. -
תיעוד —
docs/mcp-server.rst: מודל הריצה, טבלתmcp-limits(+MAX_LINES,MAX_TOKENS,WORST_CASE_CPU_SECONDS, 45), גבולות הבקשה (החשבון 45 × 0.66 = 29.7 שניות-מעבד בדקה, מתוך 30, ו-JSON עם 45),mcp-docs-path-policy(שני הסירובים החדשים + איך נבחרו;too_many_sectionsאינה נגישה ב-Markdown),mcp-docs-section, הדדליין שלread_batch(10 + 13.2 = 23.2, מתחת ל-60), ושורה 3 בפערים הידועים. וגםdocs/development/scripts.rst,docs/whats-new.rst,FEATURES_SUMMARY.md,requirements/base.txt, ה-docstrings ב-server.pyוב-read_batch.py. -
סבב 2 (
0955b3d,6f4557c) —scripts/measure_md_parse_cost.py(איפוס, probe, 40 סידורים, חסם עליון, הצמדה למעבד, כישלון בקול), הטסטים שלו, וכל המספרים בתיעוד. פירוט בסעיף "סבב סקירה 2" למטה. -
סבב 3 (
a154817) — קלטי המעבד על כל 40 הסידורים לזיכרון, ופסק הדין בודק כל מדידה ולא רק את המכריעה. פירוט בסעיף "סבב סקירה 3" למטה. -
סבב 4 (
5baaeed) — החסם של פיגור המונים נבדק רק על קרנל שהוא מוכח עליו (לינוקס מ-6.16), וטענת הקצב של תור הכתיבה ב-limits.pyנצמדת למדגם. פירוט בסעיף "סבב סקירה 4" למטה.
המספר 2.3 שניות: כל ששת המופעים החיים שלו עודכנו (שלושה ב-mcp-server.rst, limits.py, read_batch.py, docs_handlers.py). לחשבונות יש עכשיו בעלים אחד — WORST_CASE_CPU_SECONDS — ו-tests/test_md_parse_worst_case_claims.py גוזר ממנו את מגבלת הקצב, את הדדליין ואת החשבון בתיעוד.
🧪 בדיקות
- Unit
- Integration (באץ' על מראת git אמיתית, דרך השרת)
- Manual (מדידות, אפס-דיף, מוטציות)
שני התנאים שהמספרים חייבים לקיים יחד (2026-09-27, markdown-it-py 4.2.0, Python 3.11, scripts/measure_md_parse_cost.py על הקוד הסופי):
- הקלט העוין הגרוע נכנס בהקצאה לחוט: ציטוט בעומק 19 בכל אחת מ-8,000 השורות, ממולא עד 512,000 בתים,
\r\nותו אסטרלי בכל שורה, וטבלה שעוברת את התקרה בסוף הציטוט. בשיטה המתוקנת (סבב 2) השיא נע בין 34,004,992 ל-34,959,360 בתים (92.2%–94.8% מ-36,864,000), והחסם העליון — השיא הגבוה ועוד פיגור המונים של הקרנל — הוא 35,340,288 (95.9%). הריצה המלאה של סבב 2 יצאה ב-0 (passed: true); הריצה המלאה של סבב 3 — בקוד 1, על המעבד בלבד (ראו "ממצא פתוח" בסעיף סבב סקירה 3). המספרים שהיו כאן לפני סבב 2 (92.2% ו-92.6%) נמדדו בשיטה הישנה, שהסתירה עד כחצי MB. - אף קובץ אמיתי אינו נחסם: הארוך ביותר קובץ שמור של 6,151 שורות (מרווח פי 1.30), והצפוף ביותר 38,153 טוקנים בספירה מדויקת (פי 1.18). ב-
docs/עד 1,287 טוקנים, בריפו עד 8,453, ב-amir-bug-patterns עד 5,142. - המעבד: הגרוע בריצה המלאה של סבב 2 — 0.583 שניות (5 חזרות לכל קלט), מתחת ל-0.66 — כלומר 45 בדקה שומר על הטענה (לפני הדחיפה נקבע: מעל 0.667 — לעצור ולדווח). בסבב 3 נמצא שזמן המעבד על המכונה הזו משתנה במהלך היום, עד מעל 0.66 — ראו "ממצא פתוח" בסעיף סבב סקירה 3.
בדיקת ההכפלה (--doubling) — על 4.2.0 כל 30 הצורות ליניאריות. על 3.0.0 היא מסמנת את הגדרות הקישור (הזמן לבית קלט גדל פי 4.13 בין 16KB ל-64KB) ומשאירה את התבליטים ליניאריים — כלומר היא הייתה תופסת את #367 מראש. בסבב 2, עם האיפוס: אף צורה לא סומנה; זיכרון 0.90–1.29, מעבד 0.72–1.56.
אפס-דיף — הסקריפט של main על שני המצבים (main מול ה-PR), אותו קורפוס (הריפו + amir-bug-patterns 456e90b), בלי --expected-change: 524 מפות Markdown ו-208 RST, צורות עוינות, טקסט כותרות, ספירות טוקנים ו-20,666 צורות אורקל ב-12 משפחות — אפס שינויים, חוץ ממקרה סירוב אחד שהוכרע מראש: md_too_many_sections (50,001 כותרות) היה מפורסר עד הסוף ומסורב בשורה 100,003, ועכשיו נדחה ב-too_many_lines לפני הפרסור.
מוטציות — 31, כל אחת ב-worktree נפרד, כולן נתפסות על ידי טסט שבאמת נכשל. בהן: המוטציה שביקשת — הרשימה עוברת בבנאי של StateCore במקום בכלל core (עם המעקה ובלעדיו); הבדיקה אחרי הפרסור במקום ב-append; חיתוך שקט; try/except שמחזיר את מה שנבנה; תקרת השורות אחרי הפרסור; גבולות; המצב על המופע המשותף (ריצת הבקרה של טסט המקביליות, עם setswitchinterval(1e-6)); רישום הכלל אחרי החימום; שינויי 45/0.66/10; _line_of; max; האורקל עם התקרות.
T2 — כל טסט חדש נכשל על main, חוץ משני שומרים שמטבעם עוברים שם (token_count בלי תקרות; באץ' מלא בתוך המגבלה) — אותם מפילות מוטציות.
סוויטה מלאה — 6,311 עברו. 6 נכשלו: 5 הם כלים שחסרים בסביבה (isort/autopep8, כמו בסבבים הקודמים), ו-test_sticky_reminders_polling_browser.py::test_letting_every_click_render_breaks_the_test שעובר לבד גם על main וגם על ה-PR (55/55) — עומס של ההרצה המקבילית, לא קשור לשינוי. את השאר משאירים ל-CI.
mypy — שער ה-CI נקי בקומיט הראשון (3ba65cc) השער לא היה נקי, ואני כתבתי שהוא נקי: הרצתי אותו, ואחריו תיקון E501 פיצל שורה ב-_table_past ל-header = [...] בזמן ש-header כבר החזיק מספר — [return-value], בדיוק מה שהשער מחפש. claude[bot] תיקן ב-4884812, ונבדק (סבב הסקירה למטה). על העץ הנוכחי השער נקי, ובקבצים שנגעתי רק שגיאות שקיימות כבר ב-main. ruff (לא חוסם ב-CI) — פחות שורות ארוכות מ-main; נוספו רק Optional/List ב-md_parser וב-doc_sections, בכוונה, כי זה הסגנון של שני המודולים.
🧪 בדיקות נדרשות ב‑PR
- 🔍 Code Quality & Security
- Unit Tests (3.11)
- Unit Tests (3.12)
📝 סוג שינוי
- feat: פיצ'ר חדש
- fix: תיקון באג
- docs: שינוי תיעוד בלבד
- refactor: שינוי קוד ללא שינוי התנהגות
- perf: שיפור ביצועים
- chore/ci: תשתית/CI
- breaking change: שינוי שובר תאימות
✅ צ'קליסט
- הקוד עוקב אחרי הסגנון (Black/isort/flake8/mypy)
- בדיקות רצות ועוברות (הרלוונטיות; את הסוויטה המלאה — CI)
- תיעוד עודכן (README/Docs)
- אם נוספו ג'ובים חדשים — לא רלוונטי
- אם נוספו/שונו משתני סביבה – עודכן
docs/environment-variables.rstוגםservices/config_inspector_service.py(ברירת המחדל שלMCP_RATE_LIMIT_PER_MINUTE: 45) - אם נוספו/השתנו טוקנים — לא רלוונטי
- אין סודות/מפתחות בקוד
- אין מחיקות מסוכנות/פעולות על root
- הודעת הקומיט תואמת Conventional Commits
- CHANGELOG עודכן (
docs/whats-new.rst) - כל ה‑Required Checks לעיל ירוקים — ממתינים ל-CI
- צילום/וידאו UI — לא רלוונטי
- עיינתי במסמכי אתר התיעוד — נתיב:
AI-MAP.md,docs/doc-authoring.rst,docs/versioning-stable-anchors.rst,docs/mcp-server.rst(מודל הריצה,mcp-limits, גבולות הבקשה,mcp-docs-path-policy,mcp-docs-section,mcp-read-batch,mcp-md-known-divergences),docs/development/scripts.rst,docs/environment-variables.rst,docs/testing.rst,docs/style-glossary.rst| המשפט: "ספירות מופעים בפרוזה אסורות; ערכים שנאכפים מותרים" (doc-authoring) — ולכן בפרוזה נקובים שמות הקבועים, והערכים יושבים בטבלתmcp-limitsעם טסט שמשווה אותם לקוד. בסבב הסקירה נקראו שובdoc-authoringו-versioning-stable-anchorsלפני העריכה שלscripts.rst, ובסבב 2 —doc-authoring,versioning-stable-anchorsו-style-glossaryלפני העריכה שלmcp-server.rstו-whats-new.rst, ובסבב 3 —doc-authoring,versioning-stable-anchorsו-style-glossaryלפני העריכה שלscripts.rst; בסבב 4 הם לא נקראו מחדש לפני העריכה שלscripts.rst, ונקראו שוב אחרי הדחיפה. המשפט שהנחה: "מספר שחייב להופיע נושא תאריך ואת שם הסקריפט שמייצר אותו" (prose-restates-code-fact).
🧩 השפעות/סיכונים
- קובץ Markdown ארוך מ-8,000 שורות או צפוף מ-45,000 טוקנים יקבל סירוב (
too_many_lines/too_many_tokens) במקום מפה. נמדד שאין כזה היום בשום מקור שהכלי קורא, והקובץ הארוך ביותר שנמצא בכלל (שמור, 6,151 שורות) רחוק מזה ב-30%. קובץ שסורב עדיין קריא בטווחים (codekeeper_get_repo_fileעםlines). - מגבלת הקצב יורדת ל-45 בדקה לזהות (
MCP_RATE_LIMIT_PER_MINUTEלא מוגדר ב-Render, כלומר ברירת המחדל היא מה שרץ). באץ' מלא (20) עדיין נכנס. - #3466 נשאר פתוח, אבל החומרה שלו יורדת: עלות כל פרסור חסומה עכשיו בתוך הפרסר עצמו, מכל מקור שהתוכן מגיע ממנו — ולא רק כי הכלי קורא ריפואים מרשימת היתר.
- המרווח של 72 דק: 4.1%. מדידה עתידית שעוברת אותו אינה רעש, כי החסם כבר כולל את פיזור הסידורים ואת פיגור המונים. אם זה יקרה, גוזרים מחדש את הקבוע או מהדקים תקרה. מה שיזוז אז הוא רוחב המאגר (היום 10 חוטים).
- ביצועים:
appendבפייתון במקום ב-C לכל טוקן — על הקובץ השמור הצפוף ביותר לא נמדד הבדל (0.16 שניות עם התקרה ובלעדיה). עלות בניית המופע עם הכלל החדש נמדדה מחדש (0.03–0.04 מילישניות).
🔗 קישורים
- Issues קשורים: Closes #3391 · #3466 (נשאר פתוח, חומרה יורדת) · #3431 (המדידה הישנה של 2.3) · #3429 (המאגר, לא משתנה כאן — 10 חוטים, ועכשיו מוצדק גם לקלט העוין; ובאג המדידה שנולד בו תוקן כאן, בסבב 2)
- מסמכים:
תוכנית-PR-ב-תקרות-md_parser.mdב-Ck (התוכנית המאושרת) - Branch Protection & PR Rules
📌 סטיות מהתוכנית
WORST_CASE_CPU_SECONDSהוא קבוע חדש שלא היה בתוכנית. הסיבה: תוספת 2 (שישה עותקים של אותו מספר) והחלטה 3 ("טענה שאפשר לבדוק"). עכשיו יש למדידה בעלים אחד,limits.pyו-read_batch.pyנוקבים בשמו, וטסט גוזר ממנו את שתי הטענות ואת החשבון בתיעוד.docs/environment-variables.rstנערך לפני שקראתי אתdoc-authoring— סטייה בסדר העבודה. נבדק אחר כך מול הכללים, ולא נמצא מה לתקן.- הרשומה של #3431 ב-whats-new (עם 2.3 ו-60) לא שוכתבה — היא רשומה היסטורית מתוארכת; הרשומה החדשה אומרת מה השתנה.
- סקריפט המדידה חותך כל קלט לגדול ביותר שהכלי עוד מפרסר (
fit_to_the_tool: בתים ושורות). בהרצה הראשונה על הקוד הסופי הקורפוס, המסמך הצפוף וכל הצורות נדחו בתקרת השורות ונמדדו באפס — כלומר נמדד סירוב במקום פרסור. בהרצה השנייה החיתוך השאיר\rבודד בקלט עם\r\n, והסקריפט נפל בקול על זה — תוקן (_without_a_torn_crlf) ומקובע בטסט. - טסט המקביליות עבר על main בגרסה הראשונה שלו — חוט שמת בשקט (ה-
exceptעצמו זרק, כיTooManyTokensאינו קיים שם). עבר ל-Future.result(), ועכשיו הוא נכשל על main ותופס את ריצת הבקרה. באותה משפחה: מריץ המוטציות שלי ספר בהתחלה "לא נאספו טסטים" כתפיסה (בורר-kשלא התאים) — תוקן לפני הריצה הסופית. - ה-docstring הראשי של
tests/test_md_parser.pyעודכן — הוא אמר ש"אין צרכן" מאז #3428.
🔁 סבב סקירה 1 (4884812, 3b1a740)
- הקומיט של claude[bot] (
4884812) — נבדק, תקין. שינוי שם בלבד:header←header_tokensבמונה הטוקנים של_table_past. על3ba65ccmypy מחזיר שלוש שגיאות בסקריפט, ובהן[return-value]שהשער של ה-CI מחפש; על4884812— נקי. כל קלט שהסקריפט בונה (הקלט הגרוע,_table_pastבכמה תקרות, חמשת קלטי המעבד) זהה בבית לפני ואחרי, ו-31 הטסטים של הסקריפט ושל הטענות עוברים. הפספוס היה שלי: השער רץ לפני תיקון ה-E501 שהכניס את השגיאה, ולא אחריו. - Greptile —
MemoryErrorשנראה כהצלחה: נכון, תוקן ב-3b1a740. שוחזר בילד האמיתי (RLIMIT_ASשל 12MB מעל מה שהייבוא תפס):memory_errorעם שיא של 10.5MB, מתחת לתקציב, ופסק הדין אמרtrueבשתי השאלות. עכשיוMEASURED_OUTCOMES— רשימת היתר של תוצאות שהן מדידה (parsed,too_many_tokens,too_many_sections); כל תוצאה אחרת נרשמת ב-md_unmeasuredומפילה אתevery_input_measured, ו-passedהוא קוד היציאה. רשימת היתר תופסת גם אתtoo_many_lines— הבאג שכבר קרה בהרצה הראשונה על הקוד הזה (32 מתוך 38 המדידות היו סירובים, ופסק הדין עבר). זה K11, ודפוס 1 ב-BY-STACK/observability.md(המדד כולל מה שלא הוגש). - CodeRabbit — רצפת רעש לזיכרון ב-
growth: נכון, תוקן ב-3b1a740כמו שהוצע. על הקוד הקודם, שיא אפס בגודל הקטן נתן יחס של 78,125,000,000 וסימן צורה ליניארית. עכשיוMEMORY_NOISE_FLOOR_BYTES(1MiB) על השיא בגודל הקטן, ומתחתיהNoneשאינו מסמן. הטסט קיבל שם חדש,test_growth_checks_each_resource_only_above_its_noise_floor, עם הערכים הקיימים מעל הרצפה, שיא אפס, והגבול עצמו. מוטציה שהעבירה את רצפת המעבד לגודל הקטן שרדה — נוסף לה טסט. - תוספת קטנה: ה-docstring של
SUPERLINEAR_GROWTHאמר "נמדד עד 1.45" (מהאב-טיפוס); בשתי ההרצות הסופיות של--doublingהטווח הוא 0.56–1.73. (בסבב 2 עודכן שוב, לפי ההרצה עם האיפוס.) - אימות: הטסטים החדשים נופלים על
4884812(T2; המרכזיים — עלassert 0 == 1ועל יחס של 78 מיליארד); 16 מוטציות, כולן נתפסו; הרצה אמיתית של הסקריפט על3b1a740— קוד 0,passed: true, הקלט הגרוע עדיין נעצר ב-too_many_tokens(91.6%), מעבד 0.563; הרצה אמיתית של--doubling— קוד 0, אף צורה לא סומנה, ואף צורה לא איבדה את בדיקת הזיכרון (השיא הקטן ביותר בגודל הקטן 1.18MB); שער ה-mypy של ה-CI נקי על העץ הסופי; ruff — אין ממצא חדש מול HEAD. - ממצא חדש — נסגר בסבב 2 (ראו למטה): השיא נספר מעל שיא-העבר של התהליך (
max(VmRSS, VmHWM)לפני הפרסור, מאז #3429), ולכן מה שהפרסור מקצה מתחת לשיא-העבר אינו נספר לו — עד כחצי MB בקלטים של 512KB (בקטנים 0). בקלט הגרוע: נמדדו 33,771,520 בתים, ועם ה-headroom שהוחסר (452KB) — 34,234,368, כלומר 92.9% במקום 91.6%. עדיין בתוך התקציב, אבל פסק הדין מוטה לטובת "נכנס" בעד חצי MB. התיקון השורשי: לאפס אתVmHWMממש לפני הפרסור — כתיבת5ל-/proc/self/clear_refs(מאז לינוקס 4.0,proc_pid_clear_refs(5); נבדק כאן שעובד) — ואז השיא הוא בדיוק מה שהפרסור הוסיף. לא מומש, כי הוא משנה את שיטת המדידה שמתועדת ב-server.pyואת המספרים בתיעוד.
🔁 סבב סקירה 2 — שיטת המדידה (0955b3d, 6f4557c)
הבאג, והוא קיים מאז #3429: הסקריפט חישב את השיא כ"השיא אחרי הפרסור, פחות הגבוה מבין ה-RSS שלפני הפרסור לבין השיא שהתהליך כבר הגיע אליו". זיכרון שהתהליך תפס ושחרר לפני הפרסור — בעיקר החוצץ של קריאת הקובץ — הסתיר את מה שהפרסור הקצה מתחתיו: עד כחצי MB על קלט של 512KB, וכ-10MB על outline של 10MB. הטעות תמיד לכיוון "נכנס". היא נכנסה יחד עם הסקריפט עצמו, ב-5bbf0d5 של #3429.
מה השתנה בשיטה (0955b3d):
- איפוס: הילד המודד כותב
5ל-/proc/self/clear_refsרגע לפני הפרסור, וסופר את השיא מה-RSS שלפני הפרסור. לפני האיפוס הוא מרים את השיא ב-probe של 8MiB (דרךmmap, לא דרך malloc), וכך בכל ריצה מוכח שהאיפוס באמת קרה. - כישלון בקול, בלי נפילה לשיטה הישנה: קובץ שלא קיים או כתיבה שנכשלת —
SystemExitעם הודעה. כתיבה שהתקבלה בלי איפוס —SystemExit. probe קטן מכדי להבחין — עצירה. - 40 סידורי זיכרון: אותו פרסור על אותו קלט משתנה בעד כ-0.9MB לפי סידור הזיכרון שלפניו. לכן כל מדידת זיכרון רצה על
LAYOUT_SEEDS = range(40)(PYTHONHASHSEEDמ-0 עד 39), והמספר הוא המקסימום. למה 40 ולא 10: מצב שמופיע בעשירית מהסידורים חומק מעשר ריצות ביותר משליש מהפעמים (0.9 בחזקת 10), ומארבעים ריצות בפחות מ-2% (0.9 בחזקת 40). שני המספרים הם קבועים עם שם, והנימוק כתוב לידם. - חסם עליון: מה שנשפט הוא המקסימום ועוד פיגור המונים של הקרנל (
counter_lag_bound_bytes: שלושה מונים, וכל אחד מפגר עד batch−1 עמודים; כאן 380,928 בתים). - הצמדה למעבד אחד: הילד המודד מוצמד למעבד אחד, כדי שהפיגור יהיה של מעבד אחד בלבד. ההצמדה היא של המדידה, לא של השרת, והיא נכשלת בקול כשאי אפשר (אין
sched_setaffinity, אוOSError).
ריצת בקרה (תנאי 1): אותו קלט (הגרוע, כמו שהכלי מקבל אותו), אותם 40 זרעים, מוצמד, ישן וחדש לסירוגין.
| השיטה הישנה | השיטה החדשה | |
|---|---|---|
| מקסימום | 34,373,632 (93.24%) | 34,750,464 (94.27%) |
| מינימום | 33,841,152 | 34,209,792 |
ההפרש במקסימום הוא +376,832 בתים — כמעט בדיוק ה-headroom שהשיטה הישנה דיווחה והפחיתה (חציון 376KB). "הישנה ועוד ה-headroom שלה" נותנת 34,758,656, בתוך 8KB מהחדשה. אחרי האיפוס נשארו 0KB בכל 40 הריצות.
טבלת ההחלטה — ארבעת הזוגות, ישן מול חדש (40 זרעים לכל זוג, באחוזים מ-36,864,000):
| 8,000/45,000 ישן | 8,000/45,000 חדש | 7,000/50,000 ישן | 7,000/50,000 חדש | 9,000/45,000 ישן | 9,000/45,000 חדש | 8,000/50,000 ישן | 8,000/50,000 חדש | |
|---|---|---|---|---|---|---|---|---|
| מקסימום | 93.32 | 93.18 | 93.43 | 94.38 | 96.83 | 98.80 | 97.94 | 97.88 |
| חסם עליון | — | 94.21 | — | 95.41 | — | 99.83 | — | 98.91 |
הזוג הנבחר (8,000/45,000) נשאר הזול ביותר, והזוג 9,000/45,000 כמעט נוגע בתקרה. תנאי העצירה (96%) לא נדלק: כמו שהכלי מקבל אותו, הדגימה הגבוהה ביותר בכל הריצות עם האיפוס היא 34,959,360 (94.84%), והחסם העליון שלה 35,340,288 — 95.87%.
72 — ההחלטה שלך: להשאיר.
- המספר שמחזיק אותו: 69.02 בתים לכל בית של תקרת הקריאה — 95.9% מ-72, מרווח של 4.1% בלבד. זה חסם עליון ולא ממוצע: הדגימה הגבוהה ועוד פיגור המונים. הוא כתוב ליד הקבוע עם התאריך, ולידו כתוב שמדידה עתידית שעוברת את 72 אינה רעש — החסם כבר כולל את הפיזור ואת הפיגור.
- הצורה שממנה 72 נגזר ב-#3429 —
CLOUD.mdמשוכפל עד 512KB, בלי תקרות — עולה 72.4 על 4.2.0 ו-72.8 על 3.0.0 (חסם 73.2 ו-73.6), מעל 72. ה-71.7 של #3429 היה ריצה אחת מתוך טווח של 71.2–72.8. לכן הקבוע מחזיק רק בזכות התקרות של ה-PR הזה, וגם זה כתוב ליד הקבוע.
הפיזור (דרישה 2): בכל הריצות עם האיפוס, השיא של הקלט הגרוע נע בין 34,004,992 ל-34,959,360 (92.2%–94.8%). בגרסה מוקדמת של הילד 36 מ-40 הזרעים נפלו ב-92.5%–92.9% וארבעה ב-94.5%; בגרסה הסופית 39 נפלו ב-93.6%–94.3% ואחד ב-92.7%. איזה מצב שכיח תלוי בתוכנית הילד עצמה, ולכן בתיעוד כתוב הטווח ולא "ארבעה מתוך 40". מי שמריץ פעם אחת ומקבל מספר נמוך מהמתועד פגש סידור, לא שינוי.
הטסט שמוכיח שהאיפוס קרה (תנאי 5): מסמך של 4,000 פריטים, אחרי חימום של 7,000 פריטים שהשאיר שיא-עבר גבוה. השיא שנמדד קטן מהשיא של החימום, וגדול מ-1MB. בלי האיפוס (הקוד הישן), הילד מדווח 0 על פרסור שעולה בפועל 4.85–5.90MB. מוטציה שמסירה את כתיבת האיפוס מפילה את הטסט.
- מוטציות: 24, כולן נתפסות. בהן: הסרת האיפוס, הסרת הבדיקה שאחריו, איפוס לפני ה-probe, הנוסחה הישנה יחד עם האיפוס, מסכת מעבד שלא חוזרת, והסרת שמירת הרצפה ב-
--doubling. - T2: על
3b1a740(לפני הסבב) 30 מהטסטים של הסקריפט נופלים, ו-20 עוברים — אלה שלא השתנו.
כל מספר שנמדד בשיטה הישנה ומופיע בתיעוד — עודכן כאן (6f4557c: server.py, md_parser.py, mcp-server.rst, whats-new.rst, git_mirror_service.py, הסקריפט והטסטים שלו):
| מה | היה | עכשיו |
|---|---|---|
| הקלט הגרוע תחת התקרות | 92.6% | חסם עליון 95.9% (השיא 92.2%–94.8%) |
CLOUD.md משוכפל, בלי תקרות (4.2.0 / 3.0.0) | 70.1 / 71.2 | 72.4 / 72.8 (חסם 73.2 / 73.6) |
| קורפוס ה-Markdown משוכפל (4.2.0 / 3.0.0) | 20.5 / 20.8 | 24.6 / 24.7 |
| Markdown עוין, בלי תקרות | כ-290 בתים לבית, כ-142MiB | 291.9, 142.5MiB |
| קורפוס ה-RST משוכפל | "לא עולה מעל השיא שלפני הפרסור" | 2.0 בתים לבית |
| עמוד ה-RST הצפוף, משוכפל | 6.4 | 7.0 |
| RST עוין, בלי תקרה / עם 50,000 | 44.0 / 20.1MiB | 44.3 / 20.2MiB |
| outline של 10MB — הפרסור לבדו | 51MiB | 61.2MiB |
| outline של 10MB — המסלול כולו | 61–77MiB | 80.8–91.6MiB (חסם 81.2–92.0) |
lines= של הבאנדל (7MB) | כ-37MiB | 29.6MiB (חסם 29.9) |
| 12MB, לפני #3433 / אחרי | 20MiB / 0 | 22MiB / 0 |
| 7MB, מתחת לתקרה | 14MiB | 14MiB |
| תמחור לפי ה-outline היה נותן | 4 חוטים (או 9 לפי הבאנדל) | 3 חוטים |
--doubling | 0.56–1.73 | זיכרון 0.90–1.29, מעבד 0.72–1.56 |
מה לא שונה, בכוונה:
- הרשומות ההיסטוריות של #3429 ושל #3433 ב-whats-new מתעדות מה נכתב אז. הרשומה החדשה אומרת שהמספרים שם נמדדו בשיטה הישנה, ומפנה לנכונים.
- ערכים סינתטיים בטסט של המועמד (
90.2,6.4,66.6) הם קלט לטסט, ולא טענה על מדידה. - ב-docstring של
SUPERLINEAR_GROWTHנשאר ה-1.73: זה זמן מעבד מריצה מוקדמת של--doubling, והאיפוס לא משפיע על זמן מעבד. זה היחס הליניארי הגבוה ביותר שנצפה.
סטיות מהדרישות, ודברים שנמצאו בדרך:
- פיגור המונים הוא עד 372KB, ולא 128KB (דרישה 1). החסם מוכח מקוד הקרנל: ה-RSS הוא סכום של שלושה מונים, וכל אחד יכול לפגר עד 31 עמודים. ה-128KB היה רק הפיגור הגבוה שנצפה. חסם עליון חייב להיות החסם המוכח, ולכן יוצא 95.9% ולא 94.9%.
- הפיזור הפוך ממה שנכתב בדרישה 2 (ראו למעלה). בתיעוד כתוב הטווח שנצפה, ושהשכיחות תלויה בתוכנית הילד.
- אזעקת שווא שתוקנה בשורש. הריצה המלאה הראשונה עצרה בקול על
hostile.md: נשארו 100KB אחרי האיפוס, והסף היה 64KB, שניחשתי מכיול. הקרנל כותב באיפוס ערך משוער, ולכן שארית כזו היא ספירת-יתר, כלומר הכיוון הבטוח. הסף נגזר עכשיו מאותו חסם של הקרנל, ויש בדיקה בקול שה-probe גדול מספיק כדי להבחין בין איפוס שקרה לאיפוס שלא קרה. - הטווח בתיעוד תוקן אחרי
0955b3d. באימות מול הפלטים הגולמיים נמצא שהמינימום הוא 34,004,992 (מריצת הזוגות), ולא 34,037,760. - הבאנדל ירד מכ-37 ל-29.6MiB, ואין לי הסבר. השיטה הישנה רק מורידה מהשיא, כך שהמדידה הקודמת כללה משהו אחר. הרתמה המקורית לא נשמרה.
- מספרי המסלול המלא נמדדו ברתמה שאינה בריפו (הסקריפט מודד רק את הפרסור), וכך כתוב לידם. התיקון השורשי שאני מציע: להוסיף את הרתמה לריפו כסקריפט, כדי שגם המספרים האלה יהיו ניתנים לבדיקה חוזרת. לא מומש בלי אישור.
- שלוש טענות מיושנות תוקנו באותן פסקאות: "הכלי מריץ רק
rst_parser" (לא נכון מאז #3428), "הכלי מעביר לפרסר 50,000" (מאז #3420 זו ברירת המחדל של שני הפרסרים), ו"הקובץ הגדול ביותר 169KB" (היוםdocs/mcp-server.rst, מתחת ל-250KB). - שלושה עותקים של מספר הוחלפו בהפניה לבעלים: ב-
docs_handlers, ב-rst_parserובהערות של שני טסטים, לפיprose-restates-code-fact. כך מדידה חוזרת תשנה מקום אחד.
שערים, על העץ הסופי (6f4557c):
- טסטים: 1,192 טסטים קשורים עוברים. שני מודולים שדילגו כי חסר docutils הורצו אחר כך עם docutils, ו-10 הטסטים שלהם עוברים. 8 הדילוגים שנשארו דורשים מונגו אמיתי.
- flake8 החוסם: 0.
- ruff: אותם 179 ממצאים כמו ב-HEAD.
- mypy: הצעד הקפדני נקי, הפלט הרחב זהה ל-HEAD, ו-0 ממצאי
attr-defined/return-value. - בקומיט של השיטה (
0955b3d): 51 טסטים של הסקריפט, 24/24 מוטציות, ו-189 טסטים קשורים. - Sphinx: בנייה לא הורצה, לפי CLAUDE.md: אין עמוד חדש, ולא נגעתי ב-toctree. הקטעים ששונו נבדקו ב-docutils ונמצאו נקיים.
דפוסים שנקראו במלואם בסבב הזה: silent-fallback-to-worse-path, state-record-without-state-change, import-time-side-effects, line-number-coupling, widened-exception-scope, prose-restates-code-fact, host-metric-in-container, external-input-isinstance, CRITICAL-PATTERNS K11, CORE-PATTERNS U3, TESTING-PATTERNS.md ו-claude-md-snippets/testing.md. ומהאתר (לפני עריכת docs/): doc-authoring, versioning-stable-anchors, style-glossary.
🔁 סבב סקירה 3 — כיסוי הזיכרון של קלטי המעבד (a154817)
Greptile — "כיסוי חסר למדידת זיכרון": נכון, תוקן ב-a154817. קלטי המעבד רצו על חמישה סידורים בלבד (range(CPU_REPEATS)), אבל הזיכרון שלהם נכנס לפסק הדין ולמועמד לקבוע — והבדיקה שמדידה רצה על כל LAYOUT_SEEDS חלה רק על המדידה המכריעה. שוחזר על 6f4557c: קלט מעבד שהזיכרון שלו עובר את התקציב רק בזרע 39 — פסק הדין עבר, ויציאה ב-0.
השורש — למה הבדיקה הקודמת לא יכלה לתפוס את זה: מדידה על פחות סידורים נמוכה בדיוק כשהסידור היקר לא הוגרל, ואז היא גם לא נבחרת כמכריעה. בדיקה שחלה רק על המכריעה לא הייתה נדלקת אף פעם על המקרה שבשבילו נכתבה.
מה השתנה:
- קלטי המעבד רצים על כל
LAYOUT_SEEDS, כמו כל מדידה שרצה כמו הכלי. - זמן המעבד שלהם עדיין נשפט על
CPU_REPEATSהריצות הראשונות — אותם זרעים 0 עד 4 כמו קודם, כמו שהריוויוור ביקש — כיWORST_CASE_CPU_SECONDSנגזר ממקסימום על חמש ריצות לכל קלט (3ba65cc). שאר הריצות נשמרות בשורה (cpu_seconds_all), ו-cpu_timed_runsאומר כמה מהן נספרו. הנימוק כתוב לידCPU_REPEATS. verdictבודק כל מדידה כמו הכלי:md_undersampled(אילו מדידות רצו על פחות סידורים, וכמה) ו-every_input_on_every_layout, במקוםdecided_on_every_layoutו-md_worst_layout_runs, שהוסרו מהשורה האחרונה.- אף החלטת זיכרון לא זזה: על 40 סידורים, החסם העליון הגבוה מבין קלטי המעבד הוא 50.42% מהתקציב בריצה האמיתית ו-52.96% במדידה נפרדת של 21:14 (בשתיהן
lazy_quote_then_table), רחוק מהקלט הגרוע. אבל הכיסוי אינו תיאורטי: בריצה האמיתית, בארבעה מחמשת הקלטים השיא הגבוה נפל בזרע שחמשת הזרעים הראשונים לא כיסו — עד 270,336 בתים יותר (ordered_to_cap, זרע 11).
אימות:
- T2: שני הטסטים החדשים נופלים על
6f4557c—assert 0 == 1(פסק הדין עבר על שיא שלא נמדד) ו-assert True is False(מדידה חסרה שאינה המכריעה עברה). השלישי מקבע שהמעבד נשפט עלCPU_REPEATSהראשונות; על6f4557cהוא נופל עלassert 5 == 40, כי שם לא היו ריצות מאוחרות בכלל. - מוטציות: 9, כולן נתפסות — בהן החזרת
range(CPU_REPEATS)(שבעה טסטים נופלים), בדיקה של המכריעה בלבד, מעבד על כל הריצות, מעבד על האחרונות במקום הראשונות,<=במקום<, ו-passedשמתעלם מהמדידות החסרות. - ריצה אמיתית מלאה של הסקריפט על
a154817(21:31–21:46 UTC): כל 38 המדידות של Markdown שרצו כמו הכלי רצו על 40 סידורים (md_undersampledריק), וכל אחת מדדה פרסור. הזיכרון עובר: המכריע הוא עדיין הקלט הגרוע — חסם עליון 35,303,424 בתים, כלומר 95.77% (מועמד 68.95 מול 72) — וקלטי המעבד מגיעים עד 50.42%. אבל הריצה יצאה בקוד 1, בגלל המעבד — ראו את הממצא הפתוח למטה. - שערים: 53 טסטים של הסקריפט עוברים (73 יחד עם טסטי הטענות, התקצירים והעוגנים); flake8 החוסם — 0; ruff — בשני קובצי הפייתון שנגעתי, אותם ממצאים כמו ב-HEAD; mypy — הצעד הקפדני נקי, 0 ממצאי
attr-defined/return-value, ואין שגיאות בקבצים שנגעתי. בנייה של Sphinx לא הורצה (פרוזה בעמוד קיים). - main מוזג לענף (
557b898, רקCLAUDE.md). שתי שורות הטריגר החדשות נקראו:write-from-cached-readלא חל (**worstהוא מילון בזיכרון מתוצאת המדידה, בלי קאש ובלי כתיבה למסד), ודפוס 9 של mongodb לא נוגע כאן.
סטיות מהבקשה:
- הריוויוור ביקש למדוד את הזיכרון של קלטי המעבד על 40 זרעים. עשיתי את זה, ובנוסף הרחבתי את הבדיקה בפסק הדין מהמכריעה לכל מדידה — זה השורש, ובלעדיו מדידה חסרה עתידית תעבור בדיוק באותה דרך.
- שני שדות הוסרו מהשורה האחרונה (
decided_on_every_layout,md_worst_layout_runs). אין להם צרכן בקוד — רק הטסטים, שעודכנו.
⚠️ ממצא פתוח — החלטה שלך, לא שונה כאן: 0.66 אינו חסם יציב על המכונה הזו. כדי לבדוק אם אפשר לשפוט את המעבד על כל 40 הריצות, מדדתי את חמשת קלטי המעבד על 40 זרעים (21:14–21:17 UTC, מוצמד, בלי שום דבר אחר במקביל). אותם זרעים שנמדדו ב-17:4x רצו עכשיו לאט יותר ב-15–25%: ב-lazy_quote_then_table חמש הריצות הראשונות — 0.601, 0.602, 0.676, 0.582, 0.580 (ב-17:4x: 0.448 עד 0.553); על 40 ריצות המקסימום 0.871, ו-10 מתוך 40 מעל 0.66. כלומר גם הבדיקה של היום, על חמש ריצות, הייתה נופלת בשעה הזו, בלי קשר לתיקון. והריצה האמיתית על a154817 נפלה בדיוק על זה: md_worst_cpu_seconds הוא 0.824, מ-capped:deep_quote_19_lazy בזרע 4 — ריצה בודדת (החציון שלה 0.548, והבאה אחריה 0.661); ושני קלטי מעבד עוברים את 0.66 כבר בחמש הריצות הראשונות שלהם (0.669 ו-0.738). פסק הדין שופט את המעבד על בדיוק אותן ריצות כמו לפני התיקון, כך שהנפילה אינה ממנו. (בזמן הריצה הזו הרצתי במקביל פקודות קלות — git וקריאות API — שאולי הוסיפו רעש; המדידה של 21:14 רצה לבד, ומראה את אותה תמונה.) ויש כאן גם חוסר עקביות שקיים מסבב 2: שורות הזיכרון נשפטות במעבד על כל 40 הריצות, וקלטי המעבד על חמש. מגבלת הקצב (45) והדדליין של read_batch נגזרים מ-0.66, ולכן זו החלטה ולא תיקון:
- (א) — ההמלצה שלי: לשפוט את המעבד על כל 40 הריצות בכל השורות (עקבי עם הזיכרון), ולגזור מחדש את
WORST_CASE_CPU_SECONDSכחסם עליון על הפיזור, ממדידות בכמה שעות שונות — כמו שעשינו לזיכרון. עם הגבוה שנצפה היום (0.871), מגבלת הקצב שעוד שומרת על "דקת מעבד בדקה" יורדת ל-34 (30 ÷ 0.871), והדדליין נשאר בתוך 60 (10 + 17.4 = 27.4). - (ב) להשאיר חמש ריצות ו-0.66, ולכתוב ליד הקבוע שזמן המעבד על המכונה המודדת משתנה בעשרות אחוזים במהלך היום, ושפסק דין שנופל על המעבד בשעה איטית אינו בהכרח שינוי בקוד.
🔁 סבב סקירה 4 — קרנל מוכח, וטענת הקצב של תור הכתיבה (5baaeed)
CodeRabbit — "יש להוסיף בדיקת תאימות ליכולות הקרנל": נכון, תוקן. הנוסחה של counter_lag_bound_bytes נגזרה מקוד הקרנל, ובדקתי עכשיו במקור לפי גרסה על אילו גרסאות היא באמת חסם:
- מ-v6.2
mm->rss_statהואpercpu_counter, והשיא מתעדכן מ-get_mm_rss, שקורא אותו משוער. ב-v6.1 אלה מונים אטומיים, ולצידם (כשמוגדרSPLIT_RSS_COUNTING) מטמון לכל חוט שמסונכרן כל 64 אירועים — מודל אחר, שהנוסחה אינה חלה עליו. - מ-v6.16
task_memקורא את VmRSS ב-get_mm_counter_sum, כלומר מדויק. ב-v6.2, v6.6, v6.11, v6.12, v6.14 ו-v6.15 הוא קורא ב-get_mm_counter(ו-get_mm_counter_sumאינו קיים ב-mm.hשל v6.2), משוער גם הוא — ואז גם ה-RSS שלפני הפרסור סוטה, והשיא יכול לחסור עד פי שניים מהחסם. - נבדק ב-v6.16, ב-v6.18 וב-master:
compute_batch_value(max(32, 2 × המעבדים)),percpu_counter_add_batch, והאיפוס שלclear_refs— כמו שהנוסחה מניחה.
מה השתנה: counter_lag_bound_bytes מתחיל ב-_require_a_kernel_the_lag_bound_is_proven_on — לינוקס בלבד (גם ל-macOS יש release, ו-23.1.0 גדול מ-6.16), גרסה שנקראת מ-os.uname().release, ורצפה של LAG_BOUND_PROVEN_FROM_KERNEL = (6, 16). אחרת SystemExit, לפני הילד הראשון. כל שימוש בחסם עובר בפונקציה הזו, כך שגם פסק הדין וגם --doubling מוגנים, והעצירות הקיימות נשארו כמו שהן.
למה גרסה ולא בדיקה של היכולת עצמה (הסטייה היחידה מההצעה): המונים הם מבנה פנימי של הקרנל, ואין ממשק שחושף אותם. בדיקה אמפירית — השוואה של VmRSS לספירה אחרת — עוברת במקרה בכל רגע שבו מה שמחכה אצל המעבד הוא אפס, כי אז הקריאה המשוערת שווה בדיוק לסכום. רצפת גרסה לעולם אינה מקבלת קרנל ששני הדברים חסרים בו, כי הם ב-upstream מאז; לקרנל של הפצה עם מספר נמוך שקיבל אותם ב-backport היא מסרבת — וזה הכיוון הבטוח. הנימוק כתוב ליד הקבוע.
איפה הבדיקה עוברת היום: כאן (6.18.44 — החסם שנמדד לא השתנה, 380,928 בתים), ובתמונה של runner ה-ubuntu-24.04 של GitHub (6.17.0-1022-azure לפי ה-README של actions/runner-images) — שם ה-CI מריץ את הטסטים עם ילד אמיתי, ולכן הם ממשיכים לרוץ.
CodeRabbit — "צמצמו את הטענה על קצב התרוקנות התור": נכון, תוקן. "התור מתרוקן הרבה מעל המגבלה" לא נבע מהמדגם: 30 השורות מראות שבעומס של אותם ימים התור לא הצטבר, אבל לא באיזה קצב הגיעו הכתיבות. עובד אחד עומד בקצב של המגבלה רק כשגוף כתיבה נמשך בממוצע פחות מ-60 חלקי DEFAULT_RATE_LIMIT_PER_MINUTE שניות — 1.33 ב-45 — והגוף האיטי במדגם הוא 1.993. עכשיו ההערה ב-limits.py אומרת את התנאי, מה נמדד, ומה המדגם אינו מראה. הטענה הייתה ב-main עם "60 בדקה" (#3443), וה-PR הזה רק החליף את המספר ב"המגבלה".
אימות:
- T2: עשרה טסטים חדשים — תשע גרסאות קרנל (ארבע מוכחות, חמש לא), ועצירה לפני כל ילד על קרנל לא מוכח. על
a154817שישה מהם נופלים (DID NOT RAISE <class 'SystemExit'>, ו-"a measuring child was started"). - מוטציות: שש, כולן נתפסות — בלי הבדיקה,
<=במקום<, בלי בדיקת לינוקס, רצפה ב-6.2, השוואה של המספר הראשון בלבד, וגרסה שאי אפשר לקרוא שעוברת. - ילד אמיתי על הקרנל הזה: עשרת הטסטים שמריצים ילד אמיתי עוברים.
- שערים: 127 טסטים עוברים (הסקריפט,
limits, הטענות, התקצירים והעוגנים); flake8 החוסם — 0; ruff — בשלושת קובצי הפייתון שנגעתי, אותם ממצאים כמו ב-HEAD; mypy — הצעד הקפדני נקי, הפלט הרחב 1,598 כמו קודם, 0 ממצאיattr-defined/return-value, ואין שגיאות בקבצים שנגעתי. - לא אומת: קרנל ישן אמיתי — אין כאן כזה; מסלול הסירוב נבדק עם
os.unameמזויף. וגם קרנל חדש מה-master שנקרא (2026-09-27): הרצפה מקבלת אותו בלי שהקוד שלו נקרא, ואין בדיקה שתיפול בוודאות אם קרנל עתידי ישנה את ספירת המונים — כך כתוב גם ליד הקבוע.
דפוסים שנקראו במלואם — תיקון: בגרסה הקודמת של הסעיף הזה כתבתי "בסבב הזה" על כולם, וזה לא מדויק. בסבב הזה, לפני התיקון, נקראו CORE-PATTERNS U3, external-input-isinstance, host-metric-in-container ו-silent-fallback-to-worse-path. את prose-restates-code-fact, line-number-coupling, widened-exception-scope, TESTING-PATTERNS.md ו-claude-md-snippets/testing.md קראתי במלואם בסבב 3, והם היו לפניי בזמן העבודה, אבל לא קראתי אותם מחדש בסבב הזה. אחרי הדחיפה קראתי את כולם שוב, מהגרסה העדכנית של amir-bug-patterns (75c2793), יחד עם RECURRING-PATTERNS R6 (הפונקציה החדשה מפרסרת גרסת קרנל, ואין לזה עותק אחר בריפו), CRITICAL-PATTERNS K13 ו-secret-in-derived-text (בהודעות העצירה יש רק release ו-sysname, לא סוד), ו-linter-fix-changes-runtime-behavior — ובדקתי מולם את הדיף של 5baaeed: אין ממצא. ומהאתר: doc-authoring, versioning-stable-anchors ו-style-glossary נקראו בסבב 3, לפני העריכה של scripts.rst שם, ולא מחדש לפני העריכה בסבב הזה; אחרי הדחיפה קראתי אותם שוב — אין ממצא.
🧯 סיכון / החזרה לאחור (Rollback)
- Revert של הקומיט מחזיר את הפרסור לתקרת הכותרות בלבד ואת מגבלת הקצב ל-60. אין מיגרציה ואין שינוי במסד. להקלה בלי revert:
MCP_RATE_LIMIT_PER_MINUTEב-Render מחזיר מגבלה אחרת; את התקרות עצמן אפשר להזיז רק בקוד (והסקריפט בודק אותן מחדש).
🤖 Generated with Claude Code
https://claude.ai/code/session_01GQfdgfwKvZ9XNtLPs88DJf
Generated by Claude Code