Chronaxis用户手册English
本页目录

chronaxis.execution.record_session_calculations

源码基线:8904c2f744951857659abd40aaa3535d57faf53c

1"""在原AnalysisRunner会话内合并所需来源并执行会话时间指标。23共享同刻最新状态;每项指标只保自己的驱动点和归约状态。输入只读一次,4不创建普通历史、消息历史或完整贡献。均值仅保其算法必需的值与权重暂存。5每来源只保有界前瞻,同刻跨来源更新收齐后求值。所有输入真正EOF并关闭6后才交付最终结果,失败不发布已累计的前缀。7"""89from ..core.moments import SAMPLE_MOMENTS10from ..core.quantiles import SAMPLE_QUANTILES11from dataclasses import replace12from ..core.models import Interval13from functools import partial14from ..core.calculations import CalculationDefinition15from ..legacy.records import field_to_dict16from ..results.errors import ResultClosedError, AnalysisError17from ..results.spool import _reserve_owned_ram, _release_owned_ram, _cleanup_owner18from ..results.summary import SummaryCalculationRow19from ..results.summary import SummaryCoverageCalculationRow, SummaryHostedCalculationRow20from ..results.summary import SummaryEventRow21from ..core.types import QualityFlag as Q22from ..results.summary import SummarySequenceRow23from ..sources.can_spec import can_field_from_key24from .history import _metadata_bytes25from .state import LatestSignalState26from .driver_expression import DriverExpression27from .record_integral import RecordIntegral28from .record_time_calculation import RecordTimeCalculation29from .record_source_cursor import RecordSourceCursor30from .groups import DecodedTimeGroup31from .current_message_calculation import CurrentMessageCalculation32from ..legacy.sum import LegacySkipReason33from .record_rule_timeline import RecordRuleTimeline34from .record_sequences import RecordSequences35from ..planning.intervals import IntervalChoice36from .record_event_durations import RecordEventDurations,is_event_duration37from ..core.counts_v4 import OBJECT_COUNTS38from .record_object_counts import object_count_results39from .record_scope_calculations import RecordScopeCalculations40from .record_time_indexes import RecordTimeIndexes41from .record_windows import RecordWindows42from .record_exports import RecordExports43from ..results.summary import SummarySegmentRow44from ..results.summary import SummaryContributionRow45from ..results.summary import SummaryInputRow46from .record_diagnostics import RecordDiagnostics47from .record_selection import RecordSelection48from ..results.summary import SummarySelectionRow495051class RecordSessionCalculations:52    """拥有实时计算和读取子资源;计划与最终结果仓只借用。"""5354    def __init__(self, plan, budget, work_dir, *, requirements=None, declaration_owner=None, direct_integrals: bool=False):55        """预约有限声明空间,不启动读取。5657        参数: plan已校验计划、budget共享预算、work_dir已有私有运行目录;58            declaration_owner为持有共享声明的父RunSession,独立使用时省略;59            direct_integrals仅由公共入口的auto选择启用,仍需首帧前证明适用。60        返回: 新owner。异常: 构造原错附record_session_cleanup_owner供父会话接管。61        """62        if type(direct_integrals) is not bool:63            raise TypeError('直接积分内部选择需要bool')64        from ..planning.backend_selection import execution_plan65        plan = execution_plan(plan)66        self._plan, self._budget, self._path = plan, budget, work_dir67        self._declaration_owner = declaration_owner68        self._full_domain = plan.dataset.domain is None69        self._observed_domain = None70        if requirements is None:71            from ..planning.input_requirements import collect_input_requirements72            requirements=collect_input_requirements(plan)73        self._requirements=requirements74        self._held_cost = self._fixed_cost = self._group_cost = 075        self._state = self._groups = self._adapter = self._read = self._pending = None76        self._calculations = self._expressions = self._cursors = None77        self._messages = None78        # 汇总请求共享一份有界目标消息批;完整贡献保原逐消息输出次序。79        self._message_buffer = []80        self._message_cost = 081        self._message_batch_rows = (1 if plan.outputs.contributions == 'full'82                                    else min(plan.execution.batch_rows, 4096))83        self._source_ends = None84        self._rules = None85        self._sequences = None86        self._event_durations=None87        self._scope_calculations=None88        self._segment_counts = {}89        self._transform_stages = []90        self._transform_aliases = frozenset()91        self._time_indexes=self._windows=self._exports=None92        self._diagnostics=None93        self._direct_requested = direct_integrals94        self._direct_binding = self._direct_owner = None95        self._rules_begun = False96        self._constant_calculations=frozenset()97        self._spool = None98        self._aggregations = self._append_result = None99        self._next_sequence = 0100        # 指标状态随原写出计数;不为了概况回读结果仓。101        self._outcome_counts = dict(complete=0, partial=0, indeterminate=0, no_observation=0)102        self._closed = self._closing = self._failed = self._started = False103        try:104            # 本层只创建子资源槽、来源索引和计数;表达式/信号声明仍借用原计划。105            count = (len(plan.calculations) + len(plan.message_calculations) + len(plan.rules)106                     + len(plan.windows) + len(plan.sources) + len(plan.signals))107            _reserve_owned_ram(self, 131584 + 4096 * count, '_fixed_cost')108            self._calculations, self._expressions, self._cursors, self._messages = [], [], [], []109            self._source_ends = {}110            self._rules = []111        except BaseException as error:112            _cleanup_owner(self, error, 'record_session_cleanup_owner', '记录流会话构造')113            raise114115    def _new(self, factory, recovery, *args, **kwargs):116        """保留尚未交付的真实子owner。参数: 工厂/恢复属性/参数。返回: owner。异常: 原错。"""117        try:118            return factory(*args, **kwargs)119        except BaseException as error:120            self._pending = getattr(error, recovery, None)121            raise122123    def _consume_group(self, group):124        """先原子提交同刻组,再分别按所选报文推进各指标。125126        参数: 完整DecodedTimeGroup。返回: None。异常: 表达式/预算/累计原错。127        非驱动无效更新仅截断待算值,不新增周期;条件截止独立处理。128        """129        try:130            if self._diagnostics is not None:131                raw_sources={c.source_name for c in self._cursors if c._raw}132                for sample in group.samples:133                    if sample.name in self._transform_aliases:134                        continue  # 网格样本不是实际来源的报文或原字段诊断。135                    if sample.source_ref.source_name not in raw_sources:136                        values_needed=set(self._requirements.ordinary_sample_aliases)|set(self._requirements.message_aliases)137                        clock_only=sample.name not in values_needed138                        self._diagnostics.consume(sample.source_ref.source_name,sample.sequence,sample.time_ns,139                            'signal_clock' if clock_only else 'signal_sample',sample.source_ref.field,140                            {} if clock_only else {sample.name:sample.value.value},sample.value.quality)141            if self._time_indexes is not None:142                self._time_indexes.observe_group(group)143            if self._event_durations is not None:144                self._event_durations.observe_group(group)145            for owner in self._rules:146                ends=[self._source_ends[source] for source in owner.sources if source in self._source_ends]147                if len(ends)==len(owner.sources) and ends and max(ends)<group.time_ns:148                    owner.finish(max(ends))149                owner.before_group(group.time_ns)150                owner.clock_group(group)151            if self._sequences is not None:152                self._sequences.flush_before(group.time_ns)153            if self._exports is not None and self._exports._signal is not None:154                self._exports._signal.observe(group)155            if self._state is not None:156                selected=tuple(s for s in group.samples if s.name in self._state.definitions)157                if selected:158                    self._state.apply(group if len(selected)==len(group.samples) else DecodedTimeGroup(group.time_ns,selected))159            if self._scope_calculations is not None:160                self._scope_calculations.observe_group()161            if self._sequences is not None:162                self._sequences.drivers(group)163            for index,(calculation, expressions) in enumerate(zip(self._calculations, self._expressions)):164                if index in self._constant_calculations:165                    continue166                value, condition = expressions167                row = value.evaluate()168                selected = condition.evaluate() if condition is not None else None169                if value.invalidated_at is not None:170                    calculation.expire_at(value.invalidated_at)171                if condition is not None and condition.invalidated_at is not None:172                    calculation.expire_at(condition.invalidated_at, condition=True)173                if row is not None:174                    updates=({'actual_value_updates':(value.actual_update_at(row[0]),)}175                        if getattr(calculation,'_samples',False) else {})176                    if condition is None:177                        calculation.consume((row,),**updates)178                    else:179                        if selected is None or selected[0] != row[0]:180                            raise AnalysisError('值和条件未按同一驱动组求值')181                        calculation.consume((row,), conditions=(selected[1],),182                                       condition_valid_until_ns=(selected[2],),**updates)183            for owner in self._rules:184                owner.after_group(group.time_ns)185                ends=[self._source_ends[source] for source in owner.sources if source in self._source_ends]186                if len(ends)==len(owner.sources) and ends and max(ends)==group.time_ns:187                    owner.finish(max(ends))188        finally:189            group = calculation = expressions = value = condition = row = selected = None190191    def _emit_selection(self,request,parent,row):192        """按请求保存一份选择值及状态。参数: 请求/父ID/旧核借入行。返回: None。异常: 暂存错误。"""193        dataset=self._plan.dataset194        self._spool.append(SummarySelectionRow(self._next_sequence,dataset.dataset_id,dataset.entity,195            dataset.session_id,request.calculation_id,request.scope.split(':',1)[0],parent,row.time_ns,196            row.value.value if row.value.valid else None,row.value.dtype.value,row.unit,row.value.valid,197            row.selection.value,row.quality))198        self._next_sequence+=1199200    def _emit_input_trace(self,source,sequence,time_ns,record_type,locator,values,quality):201        """仅按trace请求写最终追溯表。参数: 本次必要输入定位。返回: None。异常: 暂存错误。"""202        dataset=self._plan.dataset203        self._spool.append(SummaryInputRow(self._next_sequence,dataset.dataset_id,dataset.entity,204            dataset.session_id,source,sequence,time_ns,record_type,locator,values,quality))205        self._next_sequence+=1206207    def _emit_contribution(self, request, contribution, quality):208        """仅按请求写计算定位和值/跳过。参数: 请求、借入贡献及本次质量。返回: None。异常: 暂存原错。"""209        update=contribution.update210        dataset=self._plan.dataset211        self._spool.append(SummaryContributionRow(self._next_sequence,dataset.dataset_id,dataset.entity,212            dataset.session_id,request.calculation_id,update.source,update.sequence,update.evaluation_index,213            contribution.value,contribution.skip_reason.value if contribution.skip_reason is not None else 'contributed',214            quality))215        self._next_sequence+=1216217    def execute_into(self, spool):218        """准备输入与算子,推进一次输入,并把已完成结果写入原结果仓。219220        参数: spool为本次ResultSpool。返回: None,不在这里发布文件。221        异常: 原始准备、读取或计算错误;保留可重试的record_session_cleanup_owner。222        """223        if self._closed or self._closing or self._failed or self._started:224            raise ResultClosedError('记录流会话只能执行一次')225        self._started = True226        self._spool = spool227        session_values = choices = definitions = source_cursors = state = None228        try:229            session_values, choices = self._prepare_requests(spool)230            definitions, source_cursors, state = self._prepare_input_state(choices)231            self._prepare_operators(session_values, choices, definitions, source_cursors, state)232            self._merge_sources()233            if self._direct_owner is not None:234                self._direct_owner.finish(self._source_ends.get(self._direct_binding.source))235            self._complete_results(session_values, spool)236        except BaseException as error:237            self._failed = True238            error.record_session_cleanup_owner = self239            raise240        finally:241            # 只解除编排帧对已交付声明和资源的借用,不关闭调用方结果仓。242            session_values = choices = definitions = source_cursors = state = spool = None243            self._spool = self._append_result = None244245    def _prepare_requests(self, spool):246        """从已验证计划取得请求和间隔选择;只为实际输出建立汇总状态。247248        参数: 结果仓。返回: 会话数值请求与间隔选择。异常: 汇总状态构造错误。"""249        try:250            plan = self._plan251            self._append_result = spool.append252            if plan.aggregations:253                from .result_aggregations import ResultAggregations254                self._aggregations=self._new(ResultAggregations,'aggregation_cleanup_owner',plan,self._budget)255                self._append_result=self._append_aggregate_input256            choices = dict(plan.intervals.resolved)257            hosted_reductions=plan.extensions.reductions if plan.extensions is not None else {}258            # 仅有数值输入的会话指标建立表达式和归约资源;对象计数在规则、窗口封口后产生。259            session_values = tuple(m for m in plan.calculations if m.scope == 'session'260                                   and m.operation not in OBJECT_COUNTS)261            self._segment_counts = {m.count_target: 0 for m in plan.calculations if m.operation == 'segment_count'}262            for request in plan.calculations:263                if request.operation in ('sample_count','sample_minimum','sample_maximum','sample_mean','select', *SAMPLE_MOMENTS, *SAMPLE_QUANTILES, *hosted_reductions):264                    choices[request.calculation_id]=IntervalChoice('actual')265            self._constant_calculations=frozenset(index for index,calculation in266                enumerate(session_values)267                if choices[calculation.calculation_id].driver is None and not any(268                    expression.dependencies for expression in (calculation.signal,calculation.condition) if expression is not None))269            return session_values, choices270        finally:271            # 仅放开本栈帧的临时引用;返回值已交付,子资源仍由会话所有者关闭。272            spool = plan = choices = hosted_reductions = session_values = request = None273274    def _prepare_input_state(self, choices):275        """按共用需求打开来源并建立最新信号状态,不提前读完文件。276277        参数: 已解析间隔选择。返回: 实际定义、来源游标映射和状态。278        异常: 来源、DBC周期或资源构造错误;子资源立即登记到本会话。"""279        try:280            plan = self._plan281            needed=self._requirements282            active_transforms=tuple(t for t in plan.transforms if t.name in needed.transform_names)283            self._transform_aliases=frozenset(t.name for t in active_transforms)284            if plan.diagnostics.trace or plan.diagnostics.integrity:285                self._diagnostics=self._new(RecordDiagnostics,'record_diagnostics_cleanup_owner',286                    plan,needed,self._budget,self._emit_input_trace)287            observe_bounds=needed.constant_observation288            values=set(needed.ordinary_sample_aliases)|set(needed.message_aliases)289            clocks={name for source in needed.sources for name in source.clock_aliases}290            group_names=set(needed.ordinary_sample_aliases)|clocks291            state_names=set(needed.analysis_sample_aliases)|clocks|self._transform_aliases292            export_names=set() if plan.outputs.signal_csv is None else set(plan.outputs.signal_csv.spec.signals)293            export_sources={item.definition.source for item in plan.signals if item.definition.name in export_names}294            for request in plan.message_calculations:295                values.update(request.expr.dependencies)296                if request.disjunction is not None:297                    values.add(request.disjunction.signal)298                owner = self._new(CurrentMessageCalculation, 'current_message_calculation_cleanup_owner',299                    request, plan.sources, plan.bindings, plan.dataset.session_id, self._budget,300                    backend=plan.execution.backend, batch_rows=self._message_batch_rows, domain=plan.dataset.domain,301                    contribution_consumer=(partial(self._emit_contribution,request)302                        if plan.outputs.contributions=='full' else None))303                self._messages.append((request, owner))304            from ..core.enum_codes import code_dependencies305            expressions = [expr for rule in plan.rules for expr in rule.expressions]306            expressions.extend(expr for calc in plan.calculations for expr in (calc.signal, calc.condition)307                               if expr is not None)308            expressions.extend(t.expression for t in active_transforms)309            requested_codes = {name for expr in expressions for name in code_dependencies(expr.root)}310            definitions = tuple(replace(item.definition, capture_code=True) if item.definition.name in requested_codes311                                else item.definition for item in plan.signals312                                if item.definition.name in values | clocks)313            from .same_frame_integrals import direct_integral_inputs, same_frame_binding314            direct = self._direct_requested and direct_integral_inputs(plan) is not None315            state = None316            source_cursors = {}317            for source in plan.sources:318                source_definitions = tuple(item for item in definitions if item.source == source.name)319                selectors = tuple(item.update_message for item in plan.message_calculations if item.source == source.name)320                keys = tuple(dict.fromkeys((item.channel, item.frame_id, item.is_extended, item.is_fd) for item in selectors))321                retain_frames=plan.outputs.can_frames is not None and plan.outputs.can_frames.source==source.name322                source_requirement=needed.for_source(source.name)323                if source_requirement.active:324                    cursor = self._new(RecordSourceCursor, 'record_source_cleanup_owner',325                        plan, source, source_definitions, values, clocks, self._budget,326                        self._path / ('input-' + source.name), group_names=() if direct else group_names,327                        message_consumer=self._consume_direct_update if direct else self._consume_message if keys or self._diagnostics is not None else None, message_keys=keys,observe_bounds=observe_bounds or source.name in export_sources,retain_frames=retain_frames,requirements=source_requirement)328                    self._cursors.append(cursor)329                    source_cursors[source.name] = cursor330            # 复用本次来源初始化得到的DBC周期,首帧前补齐默认有效期。331            # 不提前构造带假有效期的计算状态;无关信号不进入definitions。332            from .signal_validity import resolve_signal_validity333            definitions = resolve_signal_validity(definitions, source_cursors)334            if direct:335                cursor = source_cursors[definitions[0].source]336                self._direct_binding = same_frame_binding(definitions, cursor.metadata['fields'])337                if self._direct_binding is None:338                    # 首帧前决定普通路径;原DBC和游标保留,不再次加载或读取。339                    cursor._message_consumer = None340                    cursor._attach_raw_groups(definitions, plan.dataset.session_id, plan.execution.backend)341            definitions += tuple(t.output for t in active_transforms)342            # 由最后一层向前接回调。每层只打开自己所需状态,原Reader仍只消费一次。343            if active_transforms:344                from .transform_stream import TransformStage345                emit=self._consume_group346                for t in reversed(active_transforms):347                    choice=choices[t.name]348                    period=choice.step_ns349                    if choice.mode=='dbc':350                        definition=next(d for d in definitions if d.name==choice.driver)351                        locator=field_to_dict(can_field_from_key(definition.field))352                        period=next(f['cycle_ns'] for f in source_cursors[definition.source].metadata['fields']353                                    if f['locator']==locator)354                    stage=self._new(TransformStage,'transform_cleanup_owner',plan,t,definitions,choice,period,355                                    self._source_ends,source_cursors,self._budget,emit)356                    self._transform_stages.insert(0,stage)357                    emit=stage.push358            if self._full_domain:359                # 零长度只供尚未收到输入的数值owner构造,不作为观测或读取过滤条件。360                # 游标已用原始None请求创建;每轮前瞻后改成真实已读范围。361                plan = replace(plan, dataset=replace(plan.dataset, domain=Interval(0, 0)))362                self._plan = plan363            if self._direct_binding is None and (state_names or observe_bounds):364                state = self._state = self._new(LatestSignalState, 'state_cleanup_owner',365                    self._budget, tuple(item for item in definitions if item.name in state_names), plan.dataset.session_id,366                    declaration_owner=self._declaration_owner)367            if plan.outputs.can_frames is not None or plan.outputs.signal_csv is not None:368                self._exports=self._new(RecordExports,'record_exports_cleanup_owner',369                    plan,self._budget,self._path/'exports',self._cursors)370            return definitions, source_cursors, state371        finally:372            # 仅放开本栈帧的临时引用;返回值已交付,子资源仍由会话所有者关闭。373            choices = plan = needed = active_transforms = values = clocks = group_names = state_names = export_names = export_sources = None374            request = owner = expressions = requested_codes = definitions = state = source_cursors = source = source_definitions = selectors = keys = source_requirement = cursor = None375            emit = t = choice = definition = locator = stage = None376377    def _prepare_operators(self, session_values, choices, definitions, source_cursors, state):378        """依次建立实际请求的计算、事件、窗口和序列算子。379380        参数: 会话请求、间隔选择、实际信号定义及本次来源与状态。381        返回: None。异常: 单位、周期或资源构造错误,已构造对象仍由本会话持有。"""382        try:383            plan = self._plan384            hosted_reductions = plan.extensions.reductions if plan.extensions is not None else {}385            derived = tuple(item.definition for item in plan.derived)386            for request in session_values:387                choice = choices[request.calculation_id]388                step = choice.step_ns389                if choice.mode == 'dbc':390                    locator = field_to_dict(can_field_from_key(state.definitions[choice.driver].field))391                    metadata = source_cursors[state.definitions[choice.driver].source].metadata392                    step = next(item['cycle_ns'] for item in metadata['fields'] if item['locator'] == locator)393                expression = request.signal if request.signal is not None else request.condition394                condition = request.condition if request.signal is not None else None395                # 先登记可达槽,再构造下一个子资源,避免后续构造失败遗失前者。396                if self._direct_binding is None:397                    self._expressions.append([None, None])398                    for index, static in enumerate((expression, condition)):399                        if static is not None:400                            self._expressions[-1][index] = self._new(DriverExpression,401                                'driver_expression_cleanup_owner', static, state, choice.driver,402                                backend=plan.execution.backend, derived=derived, observation_ends=self._source_ends)403                descriptor=hosted_reductions.get(request.operation)404                spec = CalculationDefinition(request.calculation_id, 'sample_minimum' if descriptor else request.operation, plan.dataset.domain,405                                  request.partial_policy, request.unit, minimum_coverage=request.minimum_coverage,406                                  config_version=plan.config_version, ddof=request.ddof, q=request.q)407                factory = RecordIntegral if request.operation == 'integral' else RecordTimeCalculation408                recovery = 'record_integral_cleanup_owner' if request.operation == 'integral' else 'record_time_cleanup_owner'409                extra = {'work_dir': self._path} if request.operation == 'time_weighted_mean' else {}410                if descriptor:411                    extra={'hosted':descriptor}412                if request.operation=='select':413                    factory,recovery=RecordSelection,'record_selection_cleanup_owner'414                    extra={'consumer':partial(self._emit_selection,request,None)}415                self._calculations.append(self._new(factory, recovery, self._budget, spec,416                    expression.root.unit, expression.root.dtype, step, backend=plan.execution.backend,417                    batch_intervals=min(plan.execution.batch_rows, 4096), conditioned=condition is not None, **extra))418            if self._direct_binding is not None:419                from .same_frame_integrals import SameFrameIntegrals420                self._direct_owner = self._new(SameFrameIntegrals, 'same_frame_cleanup_owner',421                    self._budget, definitions, tuple(item.signal for item in session_values),422                    tuple(self._calculations), self._direct_binding, plan.dataset.session_id,423                    self._before_direct_group, derived=derived)424            metadata = None425            event_requests=tuple(item for item in plan.calculations if is_event_duration(item))426            index_ids={item.calculation_id for item in event_requests}|{item.window_id for item in plan.windows}427            drivers=tuple(dict.fromkeys(choices[name].driver for name in index_ids if choices[name].mode!='actual'))428            if drivers:429                self._time_indexes=self._new(RecordTimeIndexes,'record_time_indexes_cleanup_owner',430                    self._budget,self._path,drivers)431            if plan.windows:432                steps={}433                for request in plan.windows:434                    choice=choices[request.window_id]435                    step=choice.step_ns436                    if choice.mode=='dbc':437                        definition=state.definitions[choice.driver]438                        locator=field_to_dict(can_field_from_key(definition.field))439                        step=next(item['cycle_ns'] for item in source_cursors[definition.source].metadata['fields']440                                  if item['locator']==locator)441                    steps[request.window_id]=step442                self._windows=self._new(RecordWindows,'record_windows_cleanup_owner',443                    plan,self._budget,self._time_indexes,steps,self._emit_segment)444            if event_requests:445                steps={}446                for request in event_requests:447                    choice=choices[request.calculation_id]448                    step=choice.step_ns449                    if choice.mode=='dbc':450                        definition=state.definitions[choice.driver]451                        locator=field_to_dict(can_field_from_key(definition.field))452                        step=next(item['cycle_ns'] for item in source_cursors[definition.source].metadata['fields']453                                  if item['locator']==locator)454                    steps[request.calculation_id]=step455                self._event_durations=self._new(RecordEventDurations,'record_event_duration_cleanup_owner',456                    plan,self._budget,self._path,steps,self._emit_event_duration,indexes=self._time_indexes)457            scope_requests=tuple(item for item in plan.calculations if item.scope!='session' and not is_event_duration(item))458            if scope_requests:459                steps={}460                for request in scope_requests:461                    choice=choices[request.calculation_id]462                    step=choice.step_ns463                    if choice.mode=='dbc':464                        definition=state.definitions[choice.driver]465                        locator=field_to_dict(can_field_from_key(definition.field))466                        step=next(item['cycle_ns'] for item in source_cursors[definition.source].metadata['fields']467                                  if item['locator']==locator)468                    steps[request.calculation_id]=step469                self._scope_calculations=self._new(RecordScopeCalculations,'record_scope_calculations_cleanup_owner',470                    plan,state,self._budget,self._path,steps,scope_requests,self._source_ends,self._emit_event_duration,471                    selection_consumer=self._emit_selection)472            if plan.sequences:473                steps={}474                for request in plan.sequences:475                    choice=choices[request.sequence_id]476                    step=choice.step_ns477                    if choice.mode=='dbc':478                        definition=state.definitions[choice.driver]479                        locator=field_to_dict(can_field_from_key(definition.field))480                        step=next(item['cycle_ns'] for item in source_cursors[definition.source].metadata['fields']481                                  if item['locator']==locator)482                    steps[request.sequence_id]=step483                self._sequences=self._new(RecordSequences,'record_sequences_cleanup_owner',484                    plan,state,self._budget,steps,self._emit_sequence)485            for request in plan.rules:486                if any(p.preset_type=='threshold_excursion' and request.rule_id==p.id+'_activity' for p in plan.presets):487                    request=replace(request,stability=replace(request.stability,track_duration=True))488                choice=choices.get(request.rule_id,IntervalChoice('actual'))489                step=choice.step_ns490                if choice.mode=='dbc':491                    definition=state.definitions[choice.driver]492                    locator=field_to_dict(can_field_from_key(definition.field))493                    step=next(item['cycle_ns'] for item in source_cursors[definition.source].metadata['fields']494                              if item['locator']==locator)495                self._rules.append(self._new(RecordRuleTimeline,'record_rule_cleanup_owner',496                    plan,request,state,self._budget,step,choice.driver,self._source_ends,self._emit_event,497                    step_consumer=None if self._sequences is None else self._sequences.observe))498        finally:499            # 仅放开本栈帧的临时引用;返回值已交付,子资源仍由会话所有者关闭。500            session_values = choices = definitions = source_cursors = state = plan = hosted_reductions = derived = request = choice = metadata = expression = condition = static = None501            descriptor = spec = factory = extra = event_requests = index_ids = drivers = steps = definition = locator = scope_requests = None502503    def _complete_results(self, session_values, spool):504        """输入真正EOF后完成尾部、统计和结果汇总,不发布未完成前缀。505506        参数: 会话请求和原结果仓。返回: None。异常: 尾部计算、封存或写入错误。"""507        try:508            plan = self._plan509            # 真正EOF并关闭来源后,提交不足一批的尾部;失败不发布前缀。510            self._flush_messages()511            if self._exports is not None:512                self._exports.session()513            self._consume_constants()514            self._finish_actual_tails()515            if self._scope_calculations is not None:516                self._scope_calculations.finish(self._cursors)517            if self._event_durations is not None:518                self._event_durations.finish()519            if self._windows is not None:520                self._windows.finish(self._cursors,spool)521            if self._exports is not None:522                self._exports.seal()523            for request, calculation in zip(session_values, self._calculations):524                result = calculation.finish()525                if request.operation=='select':continue526                if self._full_domain and self._observed_domain is None:527                    result = replace(result, value=None, status='indeterminate', reason='no_observation')528                self._emit_event_duration(request,None,result)529            # 所有片段已完成合并,此时只读取事件计数及按需记录的最终片段身份数。530            for request, result in object_count_results(self._plan, self._rules, self._segment_counts):531                self._emit_event_duration(request, None, result)532            for sequence, (request, owner) in enumerate(self._messages, len(plan.calculations)):533                result = owner.snapshot()534                incomplete = any(count for reason, count in result.skip_counts.items()535                                 if reason is not LegacySkipReason.CONDITION_FALSE)536                status = 'indeterminate' if result.value is None else 'partial' if incomplete else 'complete'537                self._append_result(SummaryCalculationRow(self._next_sequence, plan.dataset.dataset_id, plan.dataset.entity,538                    plan.dataset.session_id, request.calculation_id, 'legacy_sum', 'session', None,539                    None if result.value is None else round(result.value, 6), request.unit, status,540                    result.reason or ('partial_input' if incomplete else 'ok'), owner.quality))541                self._count_outcome(status,result.reason)542                self._next_sequence+=1543            if self._aggregations is not None:544                for row in self._aggregations.rows(self._rules,self._next_sequence):545                    spool.append(row)546                    self._next_sequence+=1547        finally:548            # 仅放开本栈帧的临时引用;返回值已交付,子资源仍由会话所有者关闭。549            session_values = spool = plan = request = calculation = result = owner = row = None550551    def _append_aggregate_input(self,row):552        """从同一源行同时得到公开记录和聚合输入,不复制或重扫源表。553554        参数: row 为当前行。返回: None。异常: 原写出/归约错误终止整个会话。555        """556        self._spool.append(row)557        self._aggregations.accept(row)558559    def _finish_actual_tails(self):560        """以实际来源EOF结算连续计算和样本统计,固定周期不凭空加报文。561562        参数: 无,读取已耗尽游标、最新状态及尚待结算的末点。563        返回: None。异常: 表达式、预算或累计错误原样传播。564        不新增样本、贡献或追溯;观察结束和TTL都来自真实输入,domain仅负责裁剪。565        """566        for index, (calculation, expressions) in enumerate(zip(self._calculations, self._expressions)):567            samples = getattr(calculation, '_samples', False)568            if (index in self._constant_calculations569                    or not samples and calculation._step_ns is not None or not calculation._rows):570                continue571            value, condition = expressions572            sources = {definition.source for expression in expressions if expression is not None573                       for _, definition in expression._definitions}574            ends = [self._source_ends[source] for source in sources if source in self._source_ends]575            if not ends or max(ends) <= calculation._last_time:576                continue577            start, end = calculation._last_time, max(ends)578            # 最后组当时可能尚未知另一来源EOF;现在按右侧保持重算,不能沿用已结束来源的点值。579            current, deadline = value.evaluate_at(start)580            selected, condition_deadline = (None, None) if condition is None else condition.evaluate_at(start)581            if samples:582                # 保留真实末点及其更新标志;EOF查询只补齐有效截止,不把保持值写成新样本。583                # 某依赖来源已在末点结束时,其右侧保持无效,仍沿用既有半开EOF政策。584                previous = calculation._rows[-1]585                value_ends = tuple(t for t in (previous[2], deadline,586                    start if not current.valid else None) if t is not None)587                condition_ends = tuple(t for t in (previous[4], condition_deadline,588                    start if selected is not None and not selected.valid else None) if t is not None)589                calculation._rows[-1] = (start, previous[1], min(value_ends) if value_ends else None,590                    previous[3], min(condition_ends) if condition_ends else None) + previous[5:]591            else:592                calculation._rows[-1] = (start, current, deadline, selected, condition_deadline)593            calculation.consume(((end, current, deadline),),594                **({'actual_value_updates': (False,)} if samples else {}),595                **({} if condition is None else596                   {'conditions': (selected,), 'condition_valid_until_ns': (condition_deadline,)}))597598    def _consume_constants(self):599        """常量只需真实首末范围,EOF后复用相同统计核,不创建假信号或逐行值。600601        参数: 无。返回: None。异常: 求值/预算/计算原错;无观测不喂任何点。602        """603        if not self._constant_calculations:604            return605        starts=[c.observed_start for c in self._cursors if c.observed_start is not None]606        ends=[c.observed_end for c in self._cursors if c.observed_start is not None and c.observed_end is not None]607        if not starts or not ends:608            return609        first,last=min(starts),max(ends)610        for index in self._constant_calculations:611            calculation=self._calculations[index]612            value,condition=self._expressions[index]613            result,_=value.evaluate_at(last)614            selected=None if condition is None else condition.evaluate_at(last)[0]615            for time in dict.fromkeys((first,last)):616                # 常量的两个观测边界只说明保持范围,没有真实值更新可供计数。617                updates = {'actual_value_updates': (False,)} if calculation._spec.operation in ('sample_count', *SAMPLE_MOMENTS, *SAMPLE_QUANTILES) else {}618                calculation.consume(((time,result,None),),**updates,**({} if condition is None else619                    {'conditions':(selected,),'condition_valid_until_ns':(None,)}))620621    def _emit_event(self,event):622        """将必要事件字段直接写一次最终结果仓。参数: 原核心事件。返回: None。异常: 原写入错。"""623        try:624            plan=self._plan625            self._append_result(SummaryEventRow(self._next_sequence,plan.dataset.dataset_id,plan.dataset.entity,626                plan.dataset.session_id,event.rule.rule_id,event.event_id,event.start_ns,event.confirmed_ns,627                event.end_ns,event.status,event.trigger_kind,event.quality | (event.dependency_quality & Q.ESTIMATED)))628            self._next_sequence+=1629            if self._event_durations is not None:630                self._event_durations.observe_event(event)631            if self._scope_calculations is not None:632                self._scope_calculations.observe_event(event)633        finally:634            event=None635636    def _emit_event_duration(self,request,event_id,result):637        """把每段事件指标直接写最终指标表。参数: 请求/父ID/结果。返回: None。异常: 原发布错。"""638        plan=self._plan639        model=SummaryCalculationRow if result.coverage_status is None else SummaryCoverageCalculationRow640        extra={} if result.coverage_status is None else {'coverage_status':result.coverage_status}641        if plan.extensions is not None and request.operation in plan.extensions.reductions:642            model=SummaryHostedCalculationRow643            extra['extension_version']=plan.extensions.reductions[request.operation].version644        self._append_result(model(self._next_sequence,plan.dataset.dataset_id,plan.dataset.entity,645            plan.dataset.session_id,request.calculation_id,request.operation,request.scope.split(':',1)[0],event_id,646            result.value,result.unit,result.status,result.reason,result.quality,**extra))647        self._count_outcome(result.status,result.reason)648        self._next_sequence+=1649650    def _emit_segment(self,segment):651        """只写实际片段与父关联,后续指标借用同一片段。参数: 核心片段。返回: None。异常: 原发布/计算错。"""652        if self._exports is not None:653            self._exports.segment(segment)654        plan=self._plan655        actual=segment.actual656        self._append_result(SummarySegmentRow(self._next_sequence,plan.dataset.dataset_id,plan.dataset.entity,657            plan.dataset.session_id,segment.window_id,segment.segment_id,658            None if actual is None else actual.start_ns,None if actual is None else actual.end_ns,659            segment.status,segment.parent_event_ids,segment.quality))660        self._next_sequence+=1661        if segment.window_id in self._segment_counts:662            self._segment_counts[segment.window_id] += 1663        if self._scope_calculations is not None:664            self._scope_calculations.observe_segment(segment)665666    def _count_outcome(self,status,reason):667        """随已写出的指标计数。参数: 行状态/原因。返回: None。异常: 未支持的内部状态。"""668        self._outcome_counts[status] += 1669        if reason in ('no_observation','no_evaluations'):670            self._outcome_counts['no_observation'] += 1671672    def analysis_outcome(self):673        """取得同次规则和指标概况。参数: 无。返回: AnalysisOutcome。异常: 规则未完成。"""674        from ..results.analysis_outcome import AnalysisOutcome675        return AnalysisOutcome(tuple(owner.outcome() for owner in self._rules),**self._outcome_counts)676677    def threshold_summaries(self):678        """生成明确预设请求的有限汇总,不复制状态轨迹。参数: 无。返回: tuple。异常: 未完成统计错误。"""679        from ..results.threshold_metadata import SummaryThresholdDuration680        presets={preset.id:preset for preset in self._plan.presets if preset.preset_type=='threshold_excursion'}681        result=[]682        for owner in self._rules:683            name=owner._request.rule_id684            preset_id=name.removesuffix('_activity')685            if preset_id in presets:686                value,status=owner.duration_result687                result.append(SummaryThresholdDuration(preset_id,name,value,status))688        return tuple(result)689690    def _emit_sequence(self,result):691        """只写最终配对结论,不复制事件全文。参数: 核心结果。返回: None。异常: 原写入错。"""692        try:693            plan=self._plan694            self._append_result(SummarySequenceRow(self._next_sequence,plan.dataset.dataset_id,plan.dataset.entity,695                plan.dataset.session_id,result.rule.rule_id,result.result_id,result.request.event_id,696                None if result.response is None else result.response.event_id,result.decided_ns,697                result.delay_ns,result.status,result.observation_quality))698            self._next_sequence+=1699        finally:700            result=None701702    def _consume_message(self, update):703        """按原序收集有限目标批,供同来源各累计器共用。704705        参数: update是只借用到本回调结束的原消息;延长寿命前先付费。706        返回: None。异常: 预算、计算或取消原错;完整明细仍逐消息交付。707        """708        try:709            if self._diagnostics is not None:710                self._diagnostics.consume(update.source,update.sequence,update.time_ns,711                    'decoded_message',f'can:{update.channel}:{update.frame_id}:{int(update.is_extended)}:{int(update.is_fd)}',712                    update.values,update.quality)713            if not any(request.source == update.source for request, _ in self._messages):714                return715            if self._message_batch_rows == 1:716                for request, owner in self._messages:717                    if request.source == update.source:718                        owner.consume((update,))719                return720            # 来源切换时先提交上一批,绝不将不同来源输入混入同一求和序列。721            if self._message_buffer and self._message_buffer[0].source != update.source:722                self._flush_messages()723            cost = 16384 + 16 * _metadata_bytes(update)724            # 为下一次实际计算留出空间,不能只按缓存本身是否放得下填满预算。725            row_cost = max(owner._row_cost for request, owner in self._messages726                           if request.source == update.source)727            compute_cost = 16384 + (len(self._message_buffer) + 1) * row_cost728            # 读取器还可能构造本批返回列,不能把它尚未申请的费用当成空闲缓存。729            # 消息延长持有最多占总预算的1/16,其余仍供读取、计算和结果共用。730            buffer_limit = self._budget.memory_budget_bytes // 16731            if (self._message_buffer and (self._message_cost + cost > buffer_limit732                    or self._budget.ram_bytes + cost + compute_cost733                    > self._budget.memory_budget_bytes)):734                self._flush_messages()735            _reserve_owned_ram(self, cost, '_message_cost')736            self._message_buffer.append(update)737            if (len(self._message_buffer) >= self._message_batch_rows738                    or self._message_cost >= buffer_limit):739                self._flush_messages()740        finally:741            update = None742743    def _flush_messages(self):744        """共享当前目标批完成各汇总计算,不创建历史或逐次输出。745746        参数: 无,使用已付费的有限消息缓冲。返回: None。747        异常: 原计算/取消错误优先;清理失败保留费用,供close重试。748        """749        if not self._message_buffer:750            return751        updates = tuple(self._message_buffer)752        primary = None753        try:754            for request, owner in self._messages:755                if request.source == updates[0].source:756                    owner.consume(updates)757        except BaseException as error:758            primary = error759            raise760        finally:761            # 即使计算失败也不重放可能已经累计过的批;只允许资源清理。762            updates = None763            self._message_buffer.clear()764            try:765                _release_owned_ram(self, self._message_cost, '_message_cost')766            except BaseException as cleanup:767                if primary is None:768                    raise769                primary.add_note('目标消息批费用清理失败:' + type(cleanup).__name__)770771    def _consume_direct_update(self, update: 'LegacyDecodedUpdate') -> None:772        """向已完成首帧前检查的同帧owner交付更新。参数: 更新。返回: None。异常: 原消费错误。"""773        self._direct_owner.consume(update)774775    def _before_direct_group(self) -> None:776        """结算前更新省略domain的实际范围;显式裁剪不额外查询。参数: 无。返回: None。异常: 原读取状态错误。"""777        if not self._full_domain:778            return779        for cursor in self._cursors:780            cursor._refresh_observed_bounds()781        self._advance_observed_domain()782783    def _advance_observed_domain(self):784        """用已付费游标的真实首末更新在线范围,不读取更多输入或保存时间序列。785786        参数: 无。返回: None。显式范围保持原裁剪规则;省略范围只保一个当前并集。787        异常: 非法时间或已封闭检测器的推进错误沿原清理路径传播。788        """789        if not self._full_domain:790            return791        starts = [c.observed_start for c in self._cursors if c.observed_start is not None]792        ends = [c.observed_end for c in self._cursors if c.observed_end is not None]793        if not starts or not ends:794            return795        domain = Interval(min(starts), max(ends))796        if domain == self._observed_domain:797            return798        self._observed_domain = domain799        self._plan = replace(self._plan, dataset=replace(self._plan.dataset, domain=domain))800        for calculation in self._calculations:801            calculation._spec = replace(calculation._spec, window=domain)802            reduction = getattr(calculation, '_reduction', None)803            if reduction is not None:804                reduction._spec = calculation._spec805        for owner in self._rules:806            owner._plan = self._plan807            owner.extend_observation(domain.end_ns)808        for owner in (self._sequences, self._windows, self._scope_calculations,809                      self._event_durations, self._exports):810            if owner is not None:811                owner._plan = self._plan812        if self._exports is not None and self._exports._signal is not None:813            self._exports._signal.update_observation(self._plan)814815    def _merge_sources(self):816        """每来源只取已付费前瞻,收齐同刻更新再求值。817818        参数: 无。返回: None,所有实际来源EOF才返回。异常: 原读取/计算/预算错误。819        不按文件顺序拼接不同来源,不为跨源合并建磁盘历史。820        """821        heads = group = samples = None822        primary = None823        try:824            while True:825                heads = tuple(cursor.peek() for cursor in self._cursors)826                self._advance_observed_domain()827                for cursor in self._cursors:828                    if cursor._finished and cursor.observed_end is not None:829                        self._source_ends[cursor.source_name] = cursor.observed_end830                if self._sequences is not None:831                    self._sequences.bounds(self._cursors,self._source_ends)832                if not self._rules_begun:833                    for owner in self._rules:834                        starts=[cursor.observed_start for cursor in self._cursors835                                if cursor.source_name in owner.sources and cursor.observed_start is not None]836                        if starts:837                            owner.begin(min(starts))838                    self._rules_begun=True839                live = tuple(head for head in heads if head is not None)840                if not live:841                    for stage in self._transform_stages:842                        stage.finish()843                    for owner in self._rules:844                        ends=[self._source_ends[source] for source in owner.sources if source in self._source_ends]845                        if ends:846                            owner.finish(max(ends))847                    if self._sequences is not None:848                        self._sequences.flush_before()849                    return850                time = min(head.time_ns for head in live)851                count = sum(len(head.samples) for head in live if head.time_ns == time)852                _reserve_owned_ram(self, 16384 + count*1024, '_group_cost')853                samples = tuple(sample for head in live if head.time_ns == time for sample in head.samples)854                group = DecodedTimeGroup(time, samples)855                if self._transform_stages:856                    self._transform_stages[0].push(group)857                else:858                    self._consume_group(group)859                group = samples = live = None860                for cursor, head in zip(self._cursors, heads):861                    if head is not None and head.time_ns == time:862                        cursor.advance()863                heads = head = None864                _release_owned_ram(self, self._group_cost, '_group_cost')865        except BaseException as error:866            primary = error867            raise868        finally:869            heads = group = samples = live = head = None870            try:871                _release_owned_ram(self, self._group_cost, '_group_cost')872            except BaseException as cleanup:873                if primary is None:874                    raise875                primary.add_note('跨来源同刻组清理失败:'+type(cleanup).__name__)876877    def _close_slot(self, slot):878        """关闭成功才解除责任。参数: 槽名。返回: None。异常: 原关闭错,槽保留以便重试。"""879        owner = getattr(self, slot)880        if owner is not None:881            owner.close()882            self._require_closed(owner)883            setattr(self, slot, None)884885    @staticmethod886    def _require_closed(owner):887        """核实子资源真实关闭再撤槽。参数: owner。返回: None。异常: AnalysisError保留原责任。"""888        if not (getattr(owner, 'closed', False) or getattr(owner, '_closed', False)) or getattr(owner, '_held_cost', 0):889            raise AnalysisError('记录流子资源仍未关闭或持有费用')890891    def close(self):892        """反向释放自有资源,保留失败槽供父会话重试。893894        参数: 无。返回: None。异常: 原关闭/预算错误;不会抵销其他owner的费用。895        """896        if self._closed:897            return898        self._closing = True899        self._message_buffer.clear()900        self._segment_counts.clear()901        _release_owned_ram(self, self._message_cost, '_message_cost')902        self._close_slot('_pending')903        for stage in self._transform_stages:904            stage.close()905        self._transform_stages.clear()906        self._close_slot('_aggregations')907        self._close_slot('_diagnostics')908        self._close_slot('_direct_owner')909        self._close_slot('_exports')910        for index, owner in enumerate(self._cursors or ()):911            if owner is not None:912                owner.close()913                self._require_closed(owner)914                self._cursors[index] = None915        self._close_slot('_read')916        self._close_slot('_adapter')917        self._close_slot('_groups')918        self._close_slot('_sequences')919        self._close_slot('_event_durations')920        self._close_slot('_scope_calculations')921        self._close_slot('_windows')922        self._close_slot('_time_indexes')923        for index,owner in enumerate(self._rules or ()):924            if owner is not None:925                owner.close()926                self._require_closed(owner)927                self._rules[index]=None928        for index, pair in enumerate(self._messages or ()):929            if pair is not None:930                pair[1].close()931                self._require_closed(pair[1])932                self._messages[index] = None933        for index, owner in enumerate(self._calculations or ()):934            if owner is not None:935                owner.close()936                self._require_closed(owner)937                self._calculations[index] = None938        for pair in self._expressions or ():939            for index, owner in enumerate(pair):940                if owner is not None:941                    owner.close()942                    self._require_closed(owner)943                    pair[index] = None944        self._close_slot('_state')945        self._calculations = self._expressions = self._cursors = self._messages = self._plan = self._source_ends = None946        self._declaration_owner = None947        self._direct_binding = None948        self._rules=self._spool=self._requirements=None949        _release_owned_ram(self, self._group_cost, '_group_cost')950        _release_owned_ram(self, self._fixed_cost, '_fixed_cost')951        self._closed = True952
Chronaxis 用户手册 · 离线阅读