Chronaxis用户手册English
本页目录

chronaxis.execution.record_windows

源码基线:8904c2f744951857659abd40aaa3535d57faf53c

1"""从最终事件表生成窗口,不再复制一份事件阶段或重扫原输入。23每个窗口只保最后可能继续合并的父组及有限新父;记录周期借共享实际时刻索引。4边界扩展/合并继续使用既有两个独立窗口核,默认输出精简片段。5"""6from dataclasses import replace7from fractions import Fraction8from ..core.models import Interval9from ..core.temporal import Event,TemporalRule10from ..core.windows import WindowSpec11from ..core.windows_reference import build_segments_reference12from ..core.windows_numpy import build_segments_numpy13from ..results.summary import SummaryEventRow14from ..results.codec import decode_row15from ..core.types import QualityFlag as Q16from ..results.errors import ResultClosedError,ResultFormatError17from ..results.spool import _reserve_owned_ram,_release_owned_ram,_cleanup_owner18from .history import _metadata_bytes192021class RecordWindows:22    """拥有最终事件表读槽和有限待合并父组;共享时点索引、结果仓均为借用。"""2324    def __init__(self,plan,budget,indexes,steps,consumer):25        """预付声明,不读取输入或事件表。参数: 计划/预算/时点索引/周期/片段回调。返回: owner。异常: 原构造错。"""26        self._plan,self._budget,self._indexes,self._steps,self._consumer=plan,budget,indexes,steps,consumer27        self._held_cost=self._fixed_cost=self._parents_cost=self._reader_cost=self._workspace_cost=028        self._handle=None29        self._parents={}30        self._since={}31        self._observation={}32        self._specs={}33        self._rules={}34        self._closed=self._failed=False35        self._choices=dict(plan.intervals.resolved)36        self._coverage_needed={calculation.scope.split(":",1)[1] for calculation in plan.calculations37            if calculation.scope.startswith("segment:") and calculation.minimum_coverage>0}38        try:39            _reserve_owned_ram(self,131072+64*_metadata_bytes(plan.windows),'_fixed_cost')40            for request in plan.windows:41                self._parents[request.window_id]=[]42                self._since[request.window_id]=043                self._specs[request.window_id]=WindowSpec(request.window_id,request.event,request.anchor,44                    request.pre_ns,request.post_ns,request.merge,request.exit_delay_ns)45            for rule in plan.rules:46                self._rules[rule.rule_id]=TemporalRule(rule.rule_id,plan.dataset.dataset_id,plan.dataset.session_id,47                    rule.temporal_rule_type,rule.minimum_duration_ns or 0,rule.emit_initial,rule.window_duration_ns or 0,48                    duration_comparison=rule.duration_comparison)49            self._build=build_segments_reference if plan.execution.backend=='reference' else build_segments_numpy50        except BaseException as error:51            _cleanup_owner(self,error,'record_windows_cleanup_owner','按需窗口构造')52            raise5354    def _bounds(self,window,event):55        """按所选周期定位实际边界;actual由原核直接扩展。参数: 窗口/事件。返回: 四元组或None。异常: 原查找错。"""56        step=self._steps[window.window_id]57        if step is None:58            return None59        driver=self._choices[window.window_id].driver60        if not self._indexes.count(driver):61            return event.start_ns,event.start_ns,True,True62        end=event.start_ns if event.end_ns is None else event.end_ns63        extension=window.exit_delay_ns if window.merge=='exit_delay' else window.post_ns64        left,left_clip=self._indexes.shift(driver,event.start_ns,-window.pre_ns,step)65        right,right_clip=self._indexes.shift(driver,end,extension,step)66        return left,right,left_clip,right_clip6768    def _coverage(self,window,segment,parents):69        """仅明确门槛请求才算窗口已取得部分比例,不保存各记录的第二时间。7071        参数: 窗口/结果/本组父事件。返回: 0—1或无法确定的None。异常: 原时间读取错。72        actual沿原请求秒数;周期模式按已选记录跨度与请求周期数比较。73        """74        if segment.actual is None:75            return 0.76        ids=set(segment.parent_event_ids)77        selected=tuple(parent for parent in parents if parent.event_id in ids)78        extension=window.exit_delay_ns if window.merge=='exit_delay' else window.post_ns79        step=self._steps[window.window_id]80        if step is None:81            left=min(parent.start_ns-window.pre_ns for parent in selected)82            right=max((parent.start_ns if parent.end_ns is None else parent.end_ns)+extension for parent in selected)83            return segment.actual.duration_ns()/(right-left)84        driver=self._choices[window.window_id].driver85        starts=[]86        ends=[]87        for parent in selected:88            start,outside=self._indexes.position(driver,parent.start_ns)89            end,outside_end=self._indexes.position(driver,parent.start_ns if parent.end_ns is None else parent.end_ns)90            if outside or outside_end:91                return None92            starts.append(start-Fraction(window.pre_ns,step))93            ends.append(end+Fraction(extension,step))94        actual_left,_=self._indexes.position(driver,segment.actual.start_ns)95        actual_right,_=self._indexes.position(driver,segment.actual.end_ns)96        length=max(ends)-min(starts)97        return 0. if length==0 else min(1.,max(0.,float((actual_right-actual_left)/length)))9899    def _process(self,window,final=False):100        """复用窗口核,非末批只保最后合并组,其他片段直接输出。101102        参数: 窗口/真实EOF标识。返回: None。异常: 预算/窗口/发布原错,责任保留到close。103        """104        name=window.window_id105        entries=self._parents[name]106        if not entries:107            return108        parents=bounds=segments=None109        primary=None110        try:111            # 入队费用是8192+32*事件元数据;工作副本仍收8192+16*元数据。112            # 从已核费用推导同一金额,不重新遍历长合并组中未变化的父事件。113            workspace = 65536 + sum(4096 + item[1] // 2 for item in entries)114            _reserve_owned_ram(self,workspace,'_workspace_cost')115            parents=tuple(item[0] for item in entries)116            bounds=None if self._steps[name] is None else tuple(item[2] for item in entries)117            segments=self._build(parents,self._specs[name],self._observation[name],bounds=bounds,compact=True)118            limit=len(segments) if final else max(0,len(segments)-1)119            for index in range(limit):120                segment=segments[index]121                if name in self._coverage_needed:122                    segment=replace(segment,coverage=self._coverage(window,segment,parents))123                self._consumer(segment)124            segment=None125            keep=0126            if not final:127                first=segments[-1].parent_event_ids[0]128                keep=next(index for index,item in enumerate(entries) if item[0].event_id==first)129            else:130                keep=len(entries)131            cost=sum(item[1] for item in entries[:keep])132            del entries[:keep]133            parents=bounds=segments=None134            _release_owned_ram(self,cost,'_parents_cost')135            self._since[name]=0136        except BaseException as error:137            primary=error138            raise139        finally:140            parents=bounds=segments=None141            try:142                _release_owned_ram(self,self._workspace_cost,'_workspace_cost')143            except BaseException as cleanup:144                if primary is None:145                    raise146                # 真实写出/计算错误优先;未退费用仍在本owner,交由父会话恢复。147                primary.add_note('窗口工作区清理附加失败:'+type(cleanup).__name__)148149    def finish(self,cursors,spool):150        """所有来源结束后只读一次最终事件表,并生成片段。151152        参数: 真正EOF的游标、同次最终结果仓。返回: None。异常: 缺范围/短读/损坏/预算/发布原错。153        未请求窗口时调用方不创建本对象,空事件不建立额外中间仓。154        """155        if self._closed or self._failed:156            raise ResultClosedError('窗口已关闭或失败')157        if not spool.counts.get('events',0):158            return159        row=event=payload=None160        try:161            definitions={item.definition.name:item.definition for item in self._plan.signals}162            rules={rule.rule_id:rule for rule in self._plan.rules}163            for window in self._plan.windows:164                names=set(rules[window.event].dependencies)165                driver=self._choices[window.window_id].driver166                if driver:167                    names.add(driver)168                from ..planning.transforms import physical_dependencies169                names=physical_dependencies(self._plan,names)170                sources={definitions[name].source for name in names} if names else {source.name for source in self._plan.sources}171                starts=[c.observed_start for c in cursors if c.source_name in sources and c.observed_start is not None]172                ends=[c.observed_end for c in cursors if c.source_name in sources and c.observed_start is not None and c.observed_end is not None]173                if starts and ends:174                    left=max(min(starts),self._plan.dataset.domain.start_ns)175                    right=min(max(ends),self._plan.dataset.domain.end_ns)176                    if left<=right:177                        self._observation[window.window_id]=Interval(left,right)178            path=spool._seal_table('events')179            _reserve_owned_ram(self,spool.reader_cost_for('events'),'_reader_cost')180            self._handle=path.open('rb',buffering=8192)181            maximum=spool.row_limit('events')182            count=0183            while True:184                payload=self._handle.readline(maximum+1)185                if not payload:186                    break187                if len(payload)>maximum or not payload.endswith(b'\n'):188                    raise ResultFormatError('窗口事件输入短读或行超限')189                row=decode_row(payload,maximum)190                if type(row) is not SummaryEventRow:191                    raise ResultFormatError('窗口需要同次精简事件表')192                count+=1193                event=Event(row.event_id,self._rules[row.rule_id],'point' if row.end_ns is None else 'activity',194                    row.start_ns,row.confirmed_ns,row.end_ns,row.status,row.trigger,195                    row.quality & ~Q.ESTIMATED if row.status in ('point','timeout','closed') else row.quality,row.quality)196                for window in self._plan.windows:197                    if window.event!=row.rule_id:198                        continue199                    if window.window_id not in self._observation:200                        raise ValueError('有事件却没有实际窗口观测范围')201                    cost=8192+32*_metadata_bytes(event)202                    _reserve_owned_ram(self,cost,'_parents_cost')203                    bounds=self._bounds(window,event)204                    self._parents[window.window_id].append((event,cost,bounds))205                    self._since[window.window_id]+=1206                    if self._since[window.window_id]>=min(self._plan.execution.batch_rows,64):207                        self._process(window)208                row=event=payload=None209            if count!=spool.counts['events']:210                raise ResultFormatError('窗口事件表记录缺失')211            self._handle.close();self._handle=None212            _release_owned_ram(self,self._reader_cost,'_reader_cost')213            for window in self._plan.windows:214                self._process(window,final=True)215        except BaseException:216            self._failed=True217            raise218        finally:219            row=event=payload=None220221    @property222    def closed(self):223        """返回资源关闭状态。参数: 无。返回: bool。异常: 无。"""224        return self._closed225226    def close(self):227        """关闭读槽并释放剩余父组,不删除最终事件表。参数: 无。返回: None。异常: 原关闭错,可重试。"""228        if self._closed:229            return230        if self._handle is not None:231            self._handle.close();self._handle=None232        self._parents.clear()233        self._plan=self._consumer=self._indexes=None234        for slot in ('_parents_cost','_workspace_cost','_reader_cost','_fixed_cost'):235            _release_owned_ram(self,getattr(self,slot),slot)236        self._closed=True237
Chronaxis 用户手册 · 离线阅读