Documentation
¶
Overview ¶
Package authzformbench измеряет стоимость РЕЛЯЦИОННОЙ формы выдачи прав — работу записи, работу чтения и объём — как КРИВУЮ по N, а не как одно число.
Это измерительный прибор, а не часть продукта. Ничто под services/, pkg/, gateway/ или proto/ его не импортирует, и он ничего туда не пишет: каноническая модель `proto/kaname/cloud/iam/v1/fga_model.fga` только ЧИТАЕТСЯ.
Что здесь было и чего больше нет (S6) ¶
Пакет заводился как СРАВНЕНИЕ ШЕСТИ форм одной и той же выдачи: пяти способов разложить её в кортежи внешнего движка отношений и шестого — реляционного, где вердикт вычисляется запросом к БД. Движок снят из продукта целиком, и вместе с ним снята половина прибора: его транспорт, его посадка, преобразования канонической модели под формы C и D, вопросник согласия сторон и все пять форм хранения В НЁМ. Мерить их больше нечем — они были формами хранения в движке, и вне его не существуют.
Осталась форма E, и осталась она РАБОЧЕЙ. Сравнивать её теперь не с чем, и это сказано прямо во всех местах, где иначе читалось бы иначе: в отчёте, в правиле отнесения и в шапках проб. Что при этом НЕ пропало:
- абсолютная стоимость формы — сколько стоит выдать, отозвать, переразметить, спросить один объект и целую страницу, и сколько это занимает строк;
- НАКЛОН по N. Он и был главным утверждением замера: цена выдачи у формы E постоянна (S + 2) при том, что материализованный состав рос как N·M·S. Постоянство — результат, а не отказ прибора, и без второй стороны наклон остался единственным содержательным утверждением;
- окно неверного ответа на пути меток (`labelcost.go`) — сколько времени доступ остаётся у того, кто его уже потерял.
Предмет ¶
Правило роли сужается селектором меток. Материализующая форма разворачивала его в кортежи: N объектов × M глаголов × S субъектов — три линейных множителя, без показателя степени. При N=1000, M=5, S=10 это 50 000 строк из одной строки роли. Реляционная форма не разворачивает ничего: она хранит привязку, её субъектов и её селектор (S + 2) и вычисляет вердикт запросом на пути чтения — который горячий: страница договора до 1000 объектов.
Что измеряется и в каких единицах ¶
Запись, по операциям, — время стены И число SQL-стейтментов (величина, у которой есть объявленная ДО прогона арифметика, см. `ExpectedStatements`):
W1 grant материализовать привязку над N объектами W2 revoke снять ОДНОГО субъекта W3 relabel-1 один объект входит в набор W4 relabel-K K объектов входят в набор T-inline-* выдача и отзыв В ОДНОЙ транзакции с предметом выдачи
Чтение, по операциям, — время стены:
R1 check один объект, один глагол, один субъект R2 page-50 одна часть страницы R3 page-1000 страница договора (pkg/validate.MaxPageSize) C-cascade вопрос каскадного принципала — того, кто чинит аварию
Объём: строки выдачи и логические байты, которые Postgres относит к строкам хранилища. Накладные расходы индекса табличные, а не построковые, поэтому печатаются отдельно и никакой строке не приписываются.
Исходов ТРИ, и третий не вычитается ни в чью пользу ¶
Измерено · не выполнилось · неизмеряемое, названное текстом. «Не выполнилось» — стек не поднялся, операция вышла за срок — НИКОГДА не сворачивается в «быстро» и не опускается: оно доносится до отчёта своей строкой с причиной.
Категорий было четыре. «Отказ движка» и «неприменимо by construction» сняты вместе со своими производителями — подробности в комментарии к `Outcome`. Вторая из них была самым содержательным результатом таблицы: у движка не могло быть общей транзакции с БД предмета выдачи. У формы E эта операция выразима, поэтому она теперь ИЗМЕРЯЕТСЯ.
Прибор обязан быть показан РАЗЛИЧАЮЩИМ свой вход ¶
Прежде это доказывалось двумя способами сразу: удвоением N на форме, чья цена и есть N·M·S, и неравенством объёмов между формами. Оба способа ушли с движком, и требование от этого стало СТРОЖЕ, а не слабее: у формы E цена выдачи постоянна по N, поэтому различающую силу приходится показывать на величине, которая обязана расти, — структурной части (строка зеркала на объект). Она объявлена ДО прогона (`ExpectedStructuralRows`) и сверяется с измеренной; там же сверяется постоянство выдачи и поштучная арифметика стейтментов. Доказательство — `TestHarnessSeesTheInputOfFormE`.
Живость канала длительностей держат ТРИ пробы, а не одна, и делят её так, чтобы ни одна не спрашивала машину о её загрузке: годность пришедшей ячейки — `cellDurationChannelIsLive` (счётные и структурные свойства, зовётся из харнесса); неспособность ПРИБОРА отдать константу — `TestTimerAnswersTheWorkItWasGiven` (разница входов создаётся ожиданием, допуск назван числом); неспособность ВЫЧИСЛИТЕЛЯ схлопнуть разницу — `TestDurationChannelPropagatesADifference`. Прежде первую и вторую нёс один блок харнесса, требовавший, чтобы два замера различались, — и он ронял исправный стенд, когда часы не различали повторы (#1516).
Провенанс ¶
Отчёт называет образ Postgres, машину, ревизию дерева, дату и отпечаток канонической модели. Замер без них — не свидетельство, потому что его нельзя повторить. Модель читается и после снятия движка: план вердикта формы E компилируется из неё продуктовым `services/iam/internal/authzplan`, поэтому «на какой модели снят замер» остаётся осмысленным вопросом.
Два места снятия, и путать их нельзя ¶
Прибор держит СВОЙ Postgres (`stack.go`, один контейнер на процесс, изоляция схемой — доказательство в `isolation_test.go`) и меряет на нём воспроизведение формы E. Продуктовую форму E — запрос по настоящим таблицам iam — меряет прогон Ф5, и живёт он не здесь, а рядом с ней: правило видимости Go разрешает импортировать `services/iam/internal/...` только из дерева `services/iam/`. Прогон, положенный рядом с прибором, мерил бы воспроизведение продукта, а не продукт. Поэтому каждая ячейка несёт признак `Place`.
Index ¶
- Constants
- Variables
- func CloseSharedStack()
- func ContentOf(root, rel string) string
- func DefaultVerbs() []string
- func ExpectedGrantTuples(f Form, sc Scenario) int
- func ExpectedStatements(f Form, op Op) int
- func ExpectedStructuralRows(f Form, sc Scenario) int
- func FingerprintHeader() string
- func IsRevocation(op LabelOp) bool
- func RepoRoot() (string, error)
- func Report(w io.Writer, prov Provenance, notes map[Form]string, cfg Config, cells []Cell)
- func ReportLabelPath(w io.Writer, in LabelReportInput, cells []LabelCell)
- func Verb(v string) string
- type Atom
- type AtomKind
- type Cell
- type Census
- type Config
- type Counters
- type DirectSubject
- type FactSource
- type Fingerprint
- type Form
- type FullQuestion
- type LabelCell
- type LabelConfig
- type LabelEvent
- type LabelEventKind
- type LabelForm
- type LabelOp
- type LabelReportInput
- type LabelScenario
- type LabelWorld
- type Model
- type ModelType
- type Op
- type Outcome
- type Plan
- type ProducerStatus
- type Provenance
- type Relation
- type RelationalVerdict
- type RightsStore
- type Runner
- type SQLStmtCounter
- type Scenario
- type Stack
- type StmtWindow
- type TableNames
- type Term
- type TermKind
- type Tuple
- func CascadeSeed(_ Form, _ Scenario) []Tuple
- func Grant(f Form, sc Scenario) []Tuple
- func GrantScoped(sc Scenario, scope string) []Tuple
- func InlineIntent(sc Scenario, obj string) (data, grant []Tuple)
- func InlineRevokeIntent(sc Scenario, obj string) (data, revoke []Tuple)
- func RelabelMany(f Form, sc Scenario, objs []string) []Tuple
- func RelabelOne(f Form, sc Scenario, obj string) []Tuple
- func RevokeSubject(f Form, sc Scenario, subject string) []Tuple
- type Volume
Constants ¶
const ( MarkerComposition = "отпечаток прибора: состав " MarkerContent = "отпечаток прибора: содержимое " MarkerPredicate = "отпечаток прибора: предикат " MarkerFileList = "" /* 158-byte string literal not displayed */ MarkerFile = " " )
Marker* — приставки строк отпечатка в шапке. Читает их гейт, поэтому они объявлены здесь, рядом с писателем: разъехавшись, писатель и читатель дали бы «отпечаток не найден» на исправном отчёте.
const ( VerbPrefix = authzplan.VerbPrefix MaxPointerDepth = authzplan.MaxPointerDepth )
const ( TermDirect = authzplan.TermDirect TermComputed = authzplan.TermComputed TermTTU = authzplan.TermTTU AtomBinding = authzplan.AtomBinding AtomFact = authzplan.AtomFact )
Константы видов терма и атома — того же перечисления, что у продукта.
const AttributionRule = `` /* 6633-byte string literal not displayed */
AttributionRule — правило отнесения стоимости, объявленное ДО прогона.
Оно живёт В КОДЕ прибора, а не в прозе отчёта, ровно по одной причине: отчёт пишется после прогона, и правило, впервые появившееся в нём, неотличимо от правила, подобранного под полученные числа. Здесь оно приезжает своим коммитом, раньше первой измеренной величины, и печатается в отчёт ДОСЛОВНО отсюда — так, что расхождение между объявленным и напечатанным невозможно.
const BenchType = "vpc_network"
BenchType — тип объекта, на котором снимается замер.
Переехал сюда из снятого файла преобразований модели: там он называл тип, чьи глаголы переписывались под формы C и D, здесь — просто тип объектов фикстуры.
const CascadeChain = "leaf → project → account → cluster"
CascadeChain — объявленная цепь обхода каскада. Глубина ОГРАНИЧЕНА и это факт дерева, а не допущение: три верхних уровня доступа разрешаются каскадом намеренно (аварийный путь не должен зависеть от состояния конвейера), а вложенности групп в продукте нет.
const CascadeDepth = 3
CascadeDepth — число уровней, которые проходит соединение. Печатается рядом с измеренным временем: глубина объявляется, а не подразумевается.
const FingerprintPredicate = `все .go каталога ` + benchDir +
` — включая _test.go: харнесс прогона (сценарий, размер страницы, повторы) живёт в них,` +
` и замер, снятый другим харнессом, — другой замер`
FingerprintPredicate — предикат печатается В ШАПКУ рядом с отпечатком, чтобы читатель мог его ПОВТОРИТЬ и оспорить, а не поверить шестнадцати знакам хэша.
Отличие от предиката `scalegrid` названо прямо: там под отпечаток идут только НЕ-тестовые файлы, здесь — ВСЕ. Причина в устройстве прибора: харнесс, задающий сценарий, размер страницы и число повторов, живёт именно в `_test.go` (`run_test.go`, `harness_test.go`), и замер, снятый другим харнессом, — другой замер. Исключив их, отпечаток молчал бы на смене того, ЧТО мерили.
const QueuePredicate = `grep -rn 'KANAME_RECONCILE_DRAIN_INTERVAL_MS|PollFallback|pg_notify' ` +
`services/iam/cmd/kaname/serve.go services/iam/cmd/kaname/fga_outbox_drainer.go ` +
`services/iam/internal/repo/kaname/pg/reconcile_notify.go`
QueuePredicate — предикат, которым читается неизмеренная часть окна.
Предикатом, а не числом: число из памяти устаревает молча, команда — нет.
И НИКАКОГО «пола» из этих величин не строится. Обе очереди продукта пробуждаются УВЕДОМЛЕНИЕМ, а перечисленные периоды — запасной путь на пропущенное уведомление. Объявить период опроса нижней границей окна значило бы назвать числом то, что на штатном пути не выполняется, — придуманная величина в отчёте, чей предмет как раз в том, чтобы величины были измеренными.
Variables ¶
var ( ParseModel = authzplan.ParseModel ResolveCanonicalModel = authzplan.ResolveCanonicalModel IsVerb = authzplan.IsVerb )
var AllForms = []Form{FormE}
AllForms — перечень измеряемых форм в порядке отчёта.
var ErrPlanNotExpressible = fmt.Errorf("отношение не выражается в схеме формы E целиком")
ErrPlanNotExpressible — у отношения нет выражения в форме E целиком.
Это НЕ отказ в доступе и не сбой: третий исход, который обязан быть отличим от обоих. Свернуть его в «запрещено» значило бы напечатать совпадение там, где вопрос не задавался, — и именно так невыразимая часть модели стала бы невидимой.
Functions ¶
func CloseSharedStack ¶
func CloseSharedStack()
CloseSharedStack гасит контейнер. Безопасна, когда ничего не поднималось.
func DefaultVerbs ¶
func DefaultVerbs() []string
DefaultVerbs is the verb set 22 of the model's 24 verb-bearing types declare.
func ExpectedGrantTuples ¶
ExpectedGrantTuples — арифметика, объявленная ДО прогона, чтобы измеренное число строк сверялось с ней, а не принималось на веру.
func ExpectedStatements ¶
ExpectedStatements — объявленная ДО прогона арифметика колонки `StmtSQL` формы E, по операциям.
Она нужна не для красоты отчёта: без неё измеряемое проверялось бы только по объёму, а объём у формы E постоянен — и форма, которая ВДОБАВОК к выдаче переразмечает весь набор, прошла бы все утверждения о строках, потому что правка метки строк не добавляет. Дефект найден инъекцией собственной пробы, а не рассуждением: инъекция «форма E разворачивает набор» пережила проверку объёма и упала только на этой величине.
-1 — «не объявлена». Прежде так отвечали пять форм движка: стейтменты у них порождал он сам, и их число было свойством его реализации, а не нашим объявлением. Сегодня так отвечает только объём — у него нет колонки стейтментов, потому что это не операция замера времени.
func ExpectedStructuralRows ¶
ExpectedStructuralRows — объявленная арифметика структурной части.
Названа отдельно и до прогона, потому что у формы E ЭТО и есть величина, обязанная расти с N: цена самой выдачи у неё постоянна (s + 2), и постоянство — законный результат, а не отказ прибора. Форма, у которой ни одна величина не отвечает на удвоение входа, оставляет предпосылку «прибор различает» недоказанной, поэтому величина, которая обязана вырасти, названа заранее и сверяется с измеренной. С уходом второй стороны эта предпосылка стала ВАЖНЕЕ, а не менее важной: раньше различающую силу прибора можно было показать разницей между формами, теперь показать её нечем, кроме отклика одной формы на вход. Слагаемое 5 (а не 2) — чужой арендатор: второй аккаунт, его проект и один помеченный объект в нём. Оно названо ЗДЕСЬ, потому что эта арифметика — единственное место, где заявленный размер фикстуры сверяется с измеренным: пробы объёма упадут, если фикстура вырастет иначе, чем объявлено.
func FingerprintHeader ¶
func FingerprintHeader() string
FingerprintHeader — то, что печатает КАЖДЫЙ писатель отчёта.
Корень спрашивается у git, а не собирается из `..`: число шагов вверх зависит от того, откуда прогон запущен, и переезд вызывающего молча увёл бы отпечаток считать чужой каталог.
Отказ печатается СТРОКОЙ В ОТЧЁТ, а не глотается: отчёт без отпечатка обязан говорить, почему его нет, — иначе гейт увидит «шапки нет» и не отличит «прибор не смог» от «отчёт древний».
func IsRevocation ¶
IsRevocation — операция ли это отзыва. Перечень один, и он здесь.
func RepoRoot ¶
RepoRoot — корень дерева, спрошенный у git.
Через `pkg/gitenv`, а не прямым `exec.Command("git", …)`: унаследованные `GIT_DIR`/`GIT_INDEX_FILE` увели бы команду в чужой репозиторий — это находка отдельного гейта дерева.
func Report ¶
Report renders the matrix as a CURVE — one block per operation, forms down, N across — because a table keyed by form alone would hide the only thing that matters: whether the lines cross, and where.
Every cell prints its outcome. A "not-run" prints as such WITH its reason and is never rendered as a blank or a dash that a reader could mistake for a zero.
func ReportLabelPath ¶
func ReportLabelPath(w io.Writer, in LabelReportInput, cells []LabelCell)
ReportLabelPath печатает отчёт пути меток.
Порядок разделов: правило отнесения → провенанс → величины → окна → окна отзыва отдельно → неизмеренное. Правило первым потому, что число, прочитанное раньше правила, читатель отнесёт по своему.
Types ¶
type Cell ¶
type Cell struct {
Form Form
N int
Op Op
Outcome Outcome
Reason string
Place string // откуда снята ячейка — отчёт из двух мест без этого признака выдаёт за один прогон два
Repeats int
P50, P95, Min, Max float64 // milliseconds; zero when Outcome != Measured
// Колонка обращений была РАСЩЕПЛЕНА на две самостоятельные, пока сторон было
// две: у движка «обращение» — HTTP-вызов, за которым стоит неизвестное число
// запросов к его Postgres, у формы E — SQL-стейтмент. Складывать их было
// нельзя. Осталась одна величина; вторая снята, а не обнулена.
StmtSQL int // SQL-стейтменты
StmtNote string // непусто ⇒ StmtSQL НЕ измерен, здесь причина; печатается вместо величины
Parts int // на сколько частей разложена страница (у формы E — одна)
Tuples int // строк намерения, которых операция коснулась
GrantTotal int64 // volume only: grant rows in the store
GrantBytes int64 // volume only: logical row bytes of ALL its rows (см. Volume)
StructuralRows int64 // volume only: то, что есть у каждой формы одинаково
StructuralBytes int64
TableBytes int64 // volume only: whole relations, incl. indexes — NOT per-shape
}
Cell is one (form, N, op) result.
type Config ¶
type Config struct {
Ns []int // the curve — at least four orders, or the slope is unknown
Subjects int // S
Verbs []string
Role string
RelabelK int // K for W4
PageSize int // R3 page size (the contract page is 1000)
Partition int // objects per BatchCheck request
Parallelism int // partitions in flight
WriteRepeats int // measured repeats for write ops (warm-up is extra)
ReadRepeats int // measured samples for read ops (warm-up is extra)
Forms []Form
}
Config governs one run.
func DefaultConfig ¶
func DefaultConfig() Config
DefaultConfig mirrors the production numbers wherever one exists: the partition and parallelism are authzfilter's (50 / 8) and the page is the contract's max (pkg/validate.MaxPageSize = 1000). Where no production number exists — repeats — the value is the minimum the methodology demands, not a comfortable one.
type Counters ¶
type Counters struct {
StmtSQL int // SQL-стейтменты, снятые на Postgres
}
Counters — обращения, снятые на одной измеряемой операции.
Колонок было ДВЕ, и это было содержательно, пока сравнивались две стороны: у движка отношений «обращение» было HTTP-вызовом, за которым стояло неизвестное число запросов к его Postgres, у формы E — SQL-стейтмент. Складывать их было нельзя ни в одной ячейке.
Второй стороны больше нет (S6), и колонка обращений к движку СНЯТА, а не оставлена нулём: величина, у которой не осталось производителя, печатается нулём, неотличимым от измеренного, — ровно тот класс, против которого заведён `ProducerStatus` ниже. Осталась одна величина, и у неё есть имя.
type DirectSubject ¶
type DirectSubject = authzplan.DirectSubject
type FactSource ¶
type FactSource interface {
Names() TableNames
Query(ctx context.Context, sql string, args ...any) (pgx.Rows, error)
}
FactSource — то, поверх чего форма E вычисляет вердикт.
Интерфейс узкий намеренно: вердикт обязан быть ОДНИМ запросом, поэтому от источника нужен ровно один метод чтения и словарь имён.
type Fingerprint ¶
type Fingerprint struct {
// Composition — хэш отсортированного СПИСКА путей.
Composition string
// Content — хэш содержимого файлов в том же порядке.
Content string
// Files — сами пути, для сообщения гейта.
Files []string
// Predicate — ЧТО именно взято под отпечаток, словами.
Predicate string
}
Fingerprint — отпечаток предмета замера.
func ComputeFingerprint ¶
func ComputeFingerprint(root string) (Fingerprint, error)
ComputeFingerprint — отпечаток по дереву под root.
func (Fingerprint) FingerprintLines ¶
func (fp Fingerprint) FingerprintLines(root string) string
FingerprintLines — строки отпечатка для шапки отчёта.
Пофайлово, а не одним числом: гейт, знающий только итоговый хэш, умеет сказать «что-то сдвинулось» и не умеет сказать ЧТО — а находка без координаты требует от читателя той же работы заново.
type Form ¶
type Form string
Form — форма хранения права, которую прибор измеряет.
Значение осталось ОДНО. Их было шесть: пять описывали, как разложить выдачу в кортежи внешнего движка отношений (плоско, через группу, через отношение роли, через контейнер и композицией), шестая — реляционная — вычисляла вердикт запросом к БД. Движок снят целиком (S6), и вместе с ним снялись пять форм: они были формами хранения В НЁМ и измеряться иначе не могли.
Тип оставлен при одном значении намеренно, и это не «на будущее»: он читается — ячейка отчёта им подписана, и подпись «E-relational» отвечает на вопрос, к чему относятся числа. Отчёт, потерявший имя формы, стал бы таблицей чисел без предмета — а уже опубликованные отчёты (REPORT-*) подписаны именно так, и сопоставимость с ними держится этой подписью.
const FormE Form = "E-relational"
FormE — реляционная форма: вердикт вычисляется запросом к БД поверх зеркала объектов, внешнего движка отношений нет. Выдача — строка привязки, её субъекты и её правило-селектор: S + 2. Разворота в состав нет by construction.
type FullQuestion ¶
FullQuestion — один вопрос полного вопросника.
func (FullQuestion) Name ¶
func (q FullQuestion) Name() string
Name — имя вопроса в форме, годной для строки падения: тип, глагол, субъект, объект. Расхождение обязано называться поимённо, а не числом.
type LabelCell ¶
type LabelCell struct {
Form string
Place string
N int
Op LabelOp
Outcome Outcome
Reason string
Repeats int
P50, P95, Min, Max float64 // мс работы
StmtSQL int
StmtNote string // непусто ⇒ величина НЕ измерена, здесь причина
Rows int
Parts int
// Окно неверного ответа. Заполняется только у операций-событий.
HasWindow bool
WindowP50 float64
WindowP95 float64
WindowMax float64
WindowMin float64
Polls int
Revocation bool
}
LabelCell — одна ячейка (форма, N, операция).
func RunLabelPath ¶
func RunLabelPath(ctx context.Context, w LabelWorld, f LabelForm, sc LabelScenario, cfg LabelConfig) []LabelCell
RunLabelPath снимает все операции пути меток для ОДНОЙ формы на ОДНОМ N.
Порядок несущий: контроли → L1 (первый ответ) → установившееся чтение → события. Контроли идут первыми, потому что все остальные величины опираются на то, что форма отвечает правильно; замер, начатый без них, мерил бы скорость неверного ответа.
type LabelConfig ¶
type LabelConfig struct {
WriteRepeats int
ReadRepeats int
EventRepeats int
// WindowBudget — сколько ждать верного ответа, прежде чем признать «не
// выполнилось». Исчерпание бюджета НЕ печатается нулём.
WindowBudget time.Duration
// PollEvery — шаг опроса окна.
PollEvery time.Duration
}
LabelConfig — повторы и бюджеты. Величины, у которых нет прод-аналога, заданы минимумом, которого требует методика, а не удобным числом.
func DefaultLabelConfig ¶
func DefaultLabelConfig() LabelConfig
DefaultLabelConfig — повторов ровно столько, сколько нужно для p95 на выборке, которую не стыдно назвать выборкой.
type LabelEvent ¶
type LabelEvent struct {
Kind LabelEventKind
// ObjectID — объект события. Пусто для отзыва правила: он про весь набор.
ObjectID string
// Expect — вердикт, который новое состояние ПОДРАЗУМЕВАЕТ. Окно кончается на
// первом ответе, равном ему.
Expect bool
}
LabelEvent — одно событие пути меток.
type LabelEventKind ¶
type LabelEventKind string
LabelEventKind — что именно случилось с набором.
const ( // EventEntered — объект, уже существовавший, получил метку правила. EventEntered LabelEventKind = "метка введена" // EventCreated — объект заведён и сразу несёт метку правила. EventCreated LabelEventKind = "объект создан под правилом" // EventLeft — объект потерял метку и вышел из-под правила. EventLeft LabelEventKind = "метка снята" // EventRuleRevoked — правило отозвано; набор выходит целиком. EventRuleRevoked LabelEventKind = "правило отозвано" )
type LabelForm ¶
type LabelForm interface {
Name() string
// Place — откуда сняты ячейки этой формы. Отчёт из двух мест без этого
// признака выдаёт за один прогон два.
Place() string
// StmtProducer — чем и где считаются стейтменты этой формы и прошёл ли
// производитель контроль в обе стороны.
StmtProducer() ProducerStatus
// ApplyRule записывает правило В ТОЙ ФОРМЕ, В КАКОЙ ОНО У НЕЁ СУЩЕСТВУЕТ:
// у формы E — строки правила и выдачи.
// Это и есть предмет L1.
ApplyRule(ctx context.Context) (Counters, int, error)
// DropRule снимает правило, не трогая общее. Нужен, чтобы повтор L1 мерил
// первый ответ, а не идемпотентный повтор.
DropRule(ctx context.Context) error
// Settle — что форма ОБЯЗАНА сделать после уже закоммиченного общего
// изменения, чтобы её ответ стал верным. У формы E ожидается ноль — и он
// измеряется, а не объявляется.
Settle(ctx context.Context, ev LabelEvent) (Counters, int, error)
Check(ctx context.Context, subject, relation, objectID string) (bool, Counters, error)
// Page сужает страницу кандидатов; возвращает число разрешённых и на сколько
// частей страница была разложена.
Page(ctx context.Context, subject, relation string, ids []string) (allowed, parts int, c Counters, err error)
Teardown(ctx context.Context) error
}
LabelForm — форма, измеряемая на пути меток.
type LabelOp ¶
type LabelOp string
LabelOp — операция пути меток.
const ( // LopFirstAnswer — чего стоит ПЕРВЫЙ верный ответ, считая от момента, когда // правило написано. У формы E это запись правила и один вопрос; у формы, // материализующей состав, это была развёртка правила в кортежи — // материализация всего набора и один вопрос. Это и есть развилка N. LopFirstAnswer LabelOp = "L1-первый-ответ" // LopCheck — прямой вердикт в установившемся состоянии. LopCheck LabelOp = "D1-вердикт-1" // LopPage — страница договора (1000), суженная тем же правилом. LopPage LabelOp = "D2-страница-1000" // LopRelabelIn — метка одного объекта введена: он попадает под правило. LopRelabelIn LabelOp = "L2-метка-введена" // LopCreateInSet — объект создан и сразу попадает под существующее правило. LopCreateInSet LabelOp = "L3-создан-под-правилом" // LopRelabelOut — ОТЗЫВ: метка снята, объект выходит из-под правила. LopRelabelOut LabelOp = "L4-метка-снята-ОТЗЫВ" // LopRevokeRule — ОТЗЫВ: правило отозвано целиком, набор выходит разом. LopRevokeRule LabelOp = "L5-правило-отозвано-ОТЗЫВ" )
type LabelReportInput ¶
type LabelReportInput struct {
Prov Provenance
Scenario LabelScenario
Config LabelConfig
Ns []int
// QueueNote — что стоит между коммитом метки и началом пересчёта у продукта,
// прочитанное из дерева. ТЕКСТ, а не число: складывать его с измеренным окном
// запрещено правилом отнесения п.7.
QueueNote string
// RunCommand — чем прогон воспроизводится. Замер без него не свидетельство.
RunCommand string
// RepeatSchedule — сколько повторов снято на каждом N. Печатается строкой, а
// не одним числом: расписание, уменьшающееся с ростом N, обязано быть видно,
// иначе p95 на одном образце читается как p95 на пяти.
RepeatSchedule string
Unmeasured []string
}
LabelReportInput — всё, что нужно отчёту, кроме самих ячеек.
type LabelScenario ¶
type LabelScenario struct {
N int // объектов под правилом
Verbs []string // M — глаголы роли, в канонической форме без приставки
Subjects []string // S — субъекты выдачи, в форме "user:usr-…"
ObjectType string
ProjectID string
AccountID string
LabelKey string
LabelValue string
PageSize int // страница договора
Partition int // объектов в одном обращении BatchCheck
Parallelism int // частей страницы в полёте
}
LabelScenario — набор, правило и его размерности.
func (LabelScenario) Object ¶
func (sc LabelScenario) Object(i int) string
Object — идентификатор i-го объекта набора. Форма фиксирована: сравниваются формы, а разница в идентификаторах была бы разницей в данных.
func (LabelScenario) Ref ¶
func (sc LabelScenario) Ref(objectID string) string
Ref — объект строкой «тип:идентификатор».
func (LabelScenario) SpareCreate ¶
func (sc LabelScenario) SpareCreate() string
func (LabelScenario) SpareEnter ¶
func (sc LabelScenario) SpareEnter() string
SpareEnter / SpareCreate — объекты СОБЫТИЙ, лежащие за пределами набора.
Отдельные, а не «возьмём любой из набора»: событие, поставленное на объект набора, меняло бы сам набор от повтора к повтору, и вторая итерация мерила бы уже другой мир.
type LabelWorld ¶
type LabelWorld interface {
// Commit применяет общее изменение и возвращает МОМЕНТ его коммита — точку,
// от которой отсчитывается окно неверного ответа у КАЖДОЙ формы.
Commit(ctx context.Context, ev LabelEvent) (time.Time, error)
// Revert возвращает общее состояние к тому, каким оно было до события.
Revert(ctx context.Context, ev LabelEvent) error
}
LabelWorld — ОБЩАЯ часть обеих форм: зеркало объектов и цепь предков.
Реализуется тем, кто держит обе стороны сравнения. Границей она заведена не ради красоты: без неё общее изменение выполнял бы каждый по-своему, и правило отнесения п.2 («общее не приписывается никому») стало бы обещанием вместо свойства прогона.
type Op ¶
type Op string
Op names one measured operation.
const ( OpGrant Op = "W1-grant" // materialize the binding over N objects OpRevoke Op = "W2-revoke-1" // withdraw ONE subject OpRelabel1 Op = "W3-relabel-1" // one object enters the set OpRelabelK Op = "W4-relabel-K" // K objects enter the set OpCheck Op = "R1-check" // one object, one verb, one subject OpPage50 Op = "R2-one-partition" // ONE BatchCheck request (size = cfg.Partition) OpPageFull Op = "R3-page-full" // a whole page: cfg.PageSize, partitioned+parallel OpVolume Op = "V-volume" // tuples and bytes // Операции, которых у движка отношений нет by construction, и каскад. // // Они заведены в СЛОВАРЕ, а не описаны одной прозой: ячейка, которой нет в // словаре, не входит в сумму категорий и не печатается — отчёт был бы полон // по своему собственному счёту и молчал бы о самых дорогих вопросах. OpInlineGrant Op = "T-inline-grant" // выдача в той же транзакции, что и предмет выдачи OpInlineRevoke Op = "T-inline-revoke" // отзыв в той же транзакции OpCascade Op = "C-cascade" // вопрос каскадного принципала (три верхних уровня) )
type Outcome ¶
type Outcome string
Outcome — три категории, никогда не две.
«Не выполнилось» — то, что замер больше всего хочет потерять: форма, чей стек не поднялся, чья операция вышла за срок, чьё хранилище отвергло запрос, НЕ победила. Категория доносится до отчёта вместе с причиной, никогда не усредняется, не опускается и не засчитывается никому в пользу.
Категорий было ЧЕТЫРЕ, и обе снятые ушли вместе с движком отношений, а не были упрощены:
- `refused` — «движок ответил отказом» — был фактом о движке, и производителем его была ошибка его HTTP-API;
- `not-applicable` — «операции у формы нет by construction» — был самым содержательным результатом таблицы: у движка не могло быть общей транзакции с БД предмета выдачи. У формы E эта операция ВЫРАЗИМА, поэтому ячейка теперь измеряется, а не объявляется неприменимой.
Категория, у которой не осталось производителя, не остаётся «на всякий случай»: она печаталась бы в сводке нулём, неотличимым от посчитанного.
type ProducerStatus ¶
type ProducerStatus struct {
Place string // где снимается
Producer string // чем снимается
OK bool // прошёл ли контроль в обе стороны
Note string // что именно наблюдалось — печатается и при OK, и при провале
}
ProducerStatus — состояние ПРОИЗВОДИТЕЛЯ величины `StmtSQL` на одном месте снятия.
Величина, у входа которой нет производителя, зеленеет молча: она печатается нулём, неотличимым от измеренного. Поэтому производитель называется на каждое место снятия и считается заведённым только после контроля в обе стороны — счётчик не двигается, когда никто не спрашивает, и двигается ровно на один на одном заведомом стейтменте.
func (ProducerStatus) String ¶
func (p ProducerStatus) String() string
type Provenance ¶
type Provenance struct {
When string
TreeRev string
Machine string
Postgres string // образ, на котором СНЯТ замер
ModelPath string
ModelDigest string
// StmtProducers — состояние производителя `StmtSQL` по КАЖДОМУ месту снятия.
// Величина, у входа которой нет производителя, зеленеет молча — поэтому это
// печатается всегда, и при успехе, и при провале.
StmtProducers []ProducerStatus
// CascadeChain — объявленная цепь обхода каскада и её глубина.
CascadeChain string
CascadeDepth int
}
Provenance is what makes a number evidence rather than an anecdote.
Здесь стояли три поля движка отношений — его образ, образ его командной строки и ИЗМЕРЕННЫЙ потолок его пакетной проверки. Все три сняты вместе с ним. Потолок снят с особым сожалением и потому назван: он был измерен на живом сервере, а не взят из чужого утверждения, — ровно потому, что дерево пинило движок в двух местах разными версиями и спорить о том, чья версия верна, было не нужно, когда можно спросить. Предмета спора больше нет.
func CollectProvenance ¶
func CollectProvenance(postgres, modelPath, modelDigest string) Provenance
CollectProvenance собирает то, без чего число — анекдот.
Стека здесь больше НЕ спрашивают, и это не упрощение подписи: мест снятия два — своя посадка прибора и продуктовые таблицы iam (прогон Ф5), — и второе про стек прибора не знает вовсе. Прежняя подпись брала `*Stack` и заполняла из него образ Postgres; прогон Ф5 при этом поднимал стек, которым НЕ ПОЛЬЗОВАЛСЯ ни одной операцией, только чтобы было что передать. Образ теперь называет тот, кто на нём меряет.
type RelationalVerdict ¶
type RelationalVerdict struct {
Src FactSource
}
RelationalVerdict — вердикт формы E.
func (RelationalVerdict) Allowed ¶
func (v RelationalVerdict) Allowed(ctx context.Context, subject, verb, objectType string, objectIDs []string) (map[string]bool, error)
Allowed отвечает на вопрос о доступе для ЦЕЛОЙ страницы идентификаторов ОДНИМ запросом.
Пообъектный обход здесь не предусмотрен вовсе — и это не оптимизация, а требование к сужателю: страница продукта до тысячи объектов, и форма, которая платит за неё транзакцией на объект, измеряла бы не себя, а свой способ вызова. Одиночная проверка — та же функция на массиве из одного элемента, то есть один и тот же код на обеих полосах чтения.
type RightsStore ¶
type RightsStore interface {
// Seed кладёт структурные строки и (когда grant непуст) строки выдачи.
// Засев не входит ни в одну измеряемую величину.
Seed(ctx context.Context, structural, grant []Tuple) (Counters, error)
// Write и Remove — измеряемые мутации: выдача, переразметка, отзыв.
Write(ctx context.Context, tuples []Tuple) (Counters, error)
Remove(ctx context.Context, tuples []Tuple) (Counters, error)
// InlineGrant пишет ПРЕДМЕТ выдачи и саму выдачу в ОДНОЙ транзакции.
//
// Операция существует потому, что у формы E она ВЫРАЗИМА: выдача и её предмет
// лежат в одной базе. Прежде эта ячейка была содержательна своей пустотой у
// второй стороны — общей транзакции между БД сервиса и чужим движком не
// бывает; сегодня она измеряется как абсолютная величина, и «шесть
// стейтментов» — утверждение о форме, а не о разнице с кем-то.
InlineGrant(ctx context.Context, data, grant []Tuple) (Counters, error)
// InlineRevoke — то же для отзыва: отзыв, не зависящий от доставки.
InlineRevoke(ctx context.Context, data, revoke []Tuple) (Counters, error)
Check(ctx context.Context, subject, verb, object string) (bool, Counters, error)
BatchCheck(ctx context.Context, subject, verb string, objects []string) ([]bool, Counters, error)
// CheckPage разрешает целую страницу и возвращает, на сколько частей она была
// разложена. Число частей ОБЪЯВЛЯЕТСЯ, а не усредняется: у формы E оно равно
// единице, и единица, полученная замером, отличима от единицы по умолчанию.
CheckPage(ctx context.Context, subject, verb string, objects []string,
partition, parallel int) (verdicts []bool, parts int, c Counters, err error)
Volume(ctx context.Context) (Volume, error)
// StmtProducer называет, чем и где снимается `StmtSQL` у этого хранилища и
// прошёл ли производитель контроль в обе стороны.
StmtProducer() ProducerStatus
// Place — из какого места снята каждая ячейка этого хранилища. Отчёт,
// собранный из двух мест без этого признака, выдаёт за один прогон два.
// Признак пережил вторую сторону: места по-прежнему два — своя посадка
// прибора и продуктовые таблицы iam (прогон Ф5), — и путать их нельзя.
Place() string
Teardown(ctx context.Context) error
}
RightsStore — граница «хранилище прав»: то единственное, о чём знает измеряющий код.
Граница заводилась ради того, чтобы форма E вообще подключилась к матрице: прежний конструктор требовал модель авторизации для каждой формы, а засев возвращал конкретный тип клиента движка. Реализация у неё теперь ОДНА (`relStore`), и это не повод её снимать: она перечисляет операции, которые прибор измеряет, и держит их подписи в одном месте — измеряющий код о том, чем форма отвечает, по-прежнему не знает.
type Runner ¶
Runner executes the matrix.
func NewRunner ¶
NewRunner готовит прогон.
Прежде конструктор ГОТОВИЛ МОДЕЛИ: для каждой формы он выводил из канонического текста её DSL и гнал его через внешний преобразователь в JSON. Форме E модель не требуется — вердикт она вычисляет запросом, — и «модель не требуется» было законным ответом, отделённым от ошибки «неизвестная форма» намеренно, чтобы первое не покрывало опечатку во втором. Модели готовились для пяти форм движка; движка нет, и готовить нечего — вместе с этим сняты сам ответ-сентинел и хранилище моделей.
Конструктор оставлен, а не свёрнут в литерал: он остаётся единственным местом, где `Notes` заводится непустой картой, и вызывающему не приходится знать, что её надо создать.
func (*Runner) NewSeededStore ¶
func (r *Runner) NewSeededStore(ctx context.Context, f Form, sc Scenario, grant bool, name string) (RightsStore, error)
NewSeededStore builds a store for `f`, seeds the structural tuples and (when `grant`) the shape's grant tuples. Returned ready to be asked questions.
type SQLStmtCounter ¶
type SQLStmtCounter struct {
// contains filtered or unexported fields
}
SQLStmtCounter — счётчик стейтментов на СВОЁМ пуле pgx.
Экспортируется потому, что сторона формы E живёт там, где достижимы её таблицы, а прибор — здесь. Своего счётчика там заводить нельзя: два счётчика одного предмета расходятся молча, и расходятся они как раз в ту сторону, где расхождение не видно.
func NewSQLStmtCounter ¶
func NewSQLStmtCounter() *SQLStmtCounter
NewSQLStmtCounter заводит счётчик.
func (*SQLStmtCounter) Tracer ¶
func (c *SQLStmtCounter) Tracer() pgx.QueryTracer
Tracer — то, что вешается на конфигурацию пула.
func (*SQLStmtCounter) Verify ¶
func (c *SQLStmtCounter) Verify(ctx context.Context, pool *pgxpool.Pool, place string) ProducerStatus
Verify прогоняет контроль в ОБЕ стороны на живом пуле и возвращает состояние производителя. Пока контроль не пройден, величина не печатается вовсе — ноль от несработавшего счётчика неотличим от измеренного нуля.
type Scenario ¶
type Scenario struct {
N int // objects inside the selected set
Spare int // objects OUTSIDE it, kept for the relabel operations
Verbs []string // M — the verbs the role grants
Subjects []string // S — full principal strings, e.g. "user:u000"
Role string // "viewer" | "editor" — selects the C/D grant relation
}
Scenario is the ONE dataset every shape is measured against.
func NewScenario ¶
NewScenario builds the dataset. Subjects are `user:` principals because that is what a tenant binding names; `service_account:` resolves through the same direct userset and would not change the shape of the graph.
func (Scenario) ForeignObject ¶
ForeignObject — объект проекта ЧУЖОГО аккаунта, ПОМЕЧЕННЫЙ меткой набора.
Метка обязательна и она здесь главное. Без неё отказ на этом объекте объясняется правилом-селектором, и конъюнкт области снова остаётся непроверенным — вопрос стал бы вторым экземпляром уже покрытого «объект вне набора». С меткой у формы E выполнены ВСЕ прочие конъюнкты (субъект назван в привязке, роль даёт глагол, метка объекта под селектор попадает), и отказать обязана ровно область.
Идентификатор намеренно не из нумерации `Object(i)`: объект чужого арендатора не должен попадать ни в набор, ни в запасные — иначе он уехал бы в выдачу, страницу или переразметку и сдвинул бы базу сравнения пяти форм.
func (Scenario) LabelledInMirror ¶
LabelledInMirror — объекты, чья строка зеркала несёт метку набора.
Метку ставит ВЛАДЕЛЕЦ объекта, а не выдача: зеркало существует независимо от того, выдано ли кому-нибудь право. Поэтому чужой объект помечен так же, как свои, — и это не поблажка форме E, а единственный способ спросить её про область, не подсказав ответ другим конъюнктом.
func (Scenario) Object ¶
Object returns the id of the i-th object. Objects [0,N) are inside the selected set; [N, N+Spare) are outside it.
func (Scenario) Structural ¶
Structural returns the parent-pointer tuples. They are IDENTICAL in every shape and are therefore excluded from the grant accounting: attributing them to a shape would flatter whichever shape has fewest grant tuples.
Второй аккаунт, его проект и один объект в нём — тоже структурное: они не входят ни в набор, ни в запасные, поэтому ни одна операция записи их не касается и величина выдачи каждой формы остаётся прежней. Проверено прогоном, а не рассуждением: строки выдачи всех шести форм до и после этой правки совпали, а структурная часть выросла ровно на три строки у каждой (см. `ExpectedStructuralRows`).
type Stack ¶
type Stack struct {
// DSN — строка подключения к этой базе. Хранилища формы E берут в ней по схеме.
DSN string
// Postgres — образ, который РЕАЛЬНО поднялся, а не тот, что задумывался.
Postgres string
// contains filtered or unexported fields
}
Stack — Postgres, на котором прибор меряет форму E, и ничего кроме.
Один стек на процесс: изоляцию между кейсами даёт СХЕМА внутри этой базы, и платить контейнером за кейс значило бы платить за изоляцию, которая уже есть, больше, чем стоит измеряемое. Это же свойство держит санкцию гейта `TestNoPackageStartsAContainerPerTest`, и доказано оно исходом — `isolation_test.go`, а не объявлением здесь.
Прежде стек нёс ЧЕТЫРЕ предмета: сеть, Postgres движка отношений, сам движок и отдельный Postgres формы E. Первые три сняты вместе с движком; четвёртый остался и стал единственным. Отдельным он заводился затем, чтобы буферы, занятые кортежами движка, не наказывали чтение формы E за чужие данные, — довод пережил свой предмет, но не свою посадку: база по-прежнему своя, и числа остаются сопоставимыми с уже опубликованными отчётами.
func SharedStack ¶
SharedStack поднимает стек при первом обращении и отдаёт его каждому следующему.
Неудача подъёма возвращается КАЖДОМУ вызывающему, а не только тому, кто проиграл гонку за `Once`: иначе остальные пошли бы работать по пустому адресу и отчитались бы о форме как о «медленной» там, где не было поднято ничего.
type StmtWindow ¶
type StmtWindow struct {
// contains filtered or unexported fields
}
StmtWindow — окно счёта.
func (StmtWindow) Close ¶
func (s StmtWindow) Close() int
Close закрывает окно и возвращает число стейтментов.
type TableNames ¶
type TableNames struct {
Mirror string // зеркало объектов: тип · идентификатор · оба родителя · метки
Projects string // проект → аккаунт
Accounts string // аккаунт → кластер
Bindings string // выдача: область и роль
BindingSubjects string // субъекты выдачи
Selectors string // правило-селектор выдачи (по меткам)
RoleVerbs string // роль → глаголы, которые она даёт
AccountAdmins string // каскад, уровень 3
ClusterAdmins string // каскад, уровни 1-2
}
TableNames — имена таблиц источника фактов.
Схемы у мест снятия разные, а запрос один: то, что у синтетической схемы называется одним именем, у iam называется другим, и именно это различие выражается здесь — а не второй реализацией того же вердикта.
type Tuple ¶
Tuple — ЕДИНИЦА НАМЕРЕНИЯ: «такой-то субъект связан таким-то отношением с таким-то объектом».
Тип переехал сюда из снятого транспорта к движку отношений и потерял вместе с ним две вещи: теги JSON (сериализовать его больше некому — на wire он не уходит) и поле условия (условие на кортеже — свойство движка, и форма E его не выражает: строка не может отвечать по-разному на два одинаковых запроса).
Он остаётся ОБЩИМ СЛОВАРЁМ, а не формой хранения: производители намерения (`Grant`, `RelabelOne`, `RevokeSubject`, `InlineIntent`) говорят на нём, а хранилище переводит его в свои строки у себя. Словарь пережил свою вторую сторону намеренно — числа сегодняшних прогонов остаются сопоставимыми с уже опубликованными отчётами, потому что намерение осталось тем же.
func CascadeSeed ¶
CascadeSeed — принципал трёх верхних уровней доступа.
Строка администратора аккаунта, которую запрос вердикта достаёт соединением ограниченной глубины по родительским указателям (leaf → project → account → cluster). Каскад меряется отдельной ячейкой, потому что это и есть путь, которым чинят аварию: он разрешается в момент запроса и не зависит от того, доехала ли до кого-нибудь материализация.
func Grant ¶
Grant returns the tuples that materialize the binding over the in-set objects.
Выдача формы E — строка привязки, её субъекты и её правило-селектор. Объекты набора здесь НЕ перечисляются: метка живёт в зеркале, зеркало наполняют владельцы объектов, и выдача узнаёт набор запросом. Отсюда и то, что «выдать» у неё стоит S + 2 независимо от N — постоянство и есть измеряемый результат, а не отказ прибора.
func GrantScoped ¶
GrantScoped — та же выдача, но на названной области.
Существует ради аккаунтной ветви предиката области, и «проверяется» здесь означает УТВЕРЖДЕНИЕ, а не исполнение. У ветви есть пара: TestAccountScopedGrantReachesObjectsOfItsProjects требует положительного (аккаунтная выдача накрывает объект проекта СВОЕГО аккаунта) и отрицательного (не накрывает помеченный объект проекта ЧУЖОГО аккаунта). Прежняя редакция этого комментария обещала проверку, которой не было: ветвь исполнялась, но ни одно утверждение от неё не зависело — снятие корреляции области не роняло ни одной пробы.
func InlineIntent ¶
InlineIntent — предмет выдачи и сама выдача для операций «в одной транзакции».
Предмет — новый объект (строка зеркала), выдача — привязка на него. У движка отношений эта пара неприменима by construction, и ячейка сообщает именно это.
func InlineRevokeIntent ¶
InlineRevokeIntent — отзыв одного субъекта встраиваемой привязки, в той же транзакции, что и правка предмета выдачи.
func RelabelMany ¶
RelabelMany is RelabelOne applied to K objects — the "mass re-tagging" operation.
func RelabelOne ¶
RelabelOne returns the tuples written when ONE object ENTERS the selected set — the "a label changed on one resource" operation.
Это ось, на которой формы расходились сильнее всего и на которой плоский индекс платил хуже прочих: он переписывал всё произведение глаголов на субъекты для этого объекта. У формы E это правка метки ОДНОЙ строки зеркала, и величина от N не зависит.
func RevokeSubject ¶
RevokeSubject returns the tuples DELETED when ONE subject loses the binding.
Отзыв меряется отдельно от выдачи намеренно: путь материализации, умеющий только ДОБАВЛЯТЬ, зелен на каждом утверждении «было ли выдано» и неверен ровно на той операции, ради которой отзыв существует (см. .claude/rules/testing.md §«Параллельный newman», дискриминатор create-vs-update).
type Volume ¶
type Volume struct {
GrantRows int64
GrantBytes int64
StructuralRows int64
StructuralBytes int64
TableBytes int64
}
Volume — ёмкостная сторона ячейки.
Смысл колонок закреплён здесь, потому что у двух из них он неочевиден:
- GrantRows — строки ВЫДАЧИ: привязка, её субъекты и её правило-селектор;
- GrantBytes — логические байты ВСЕХ строк хранилища формы, включая структурные. Асимметрия «строки без структурных, байты со структурными» унаследована от снятой базы сравнения и сохранена намеренно: числа сегодняшних прогонов остаются сопоставимыми с уже опубликованными отчётами (REPORT-*), которые считались этим же правилом. Чтобы асимметрия не читалась молча, структурная часть вынесена в соседнюю пару колонок и печатается рядом;
- StructuralRows/Bytes — зеркало объектов и цепь предков: то, что у формы есть независимо от того, выдано ли кому-нибудь право;
- TableBytes — весь размер отношений с индексами. Индекс табличный, а не построковый, поэтому на строку он не делится и печатается отдельно.