Windowing TVF 源码

窗口样例

-- TUMBLE/HOP/CUMULATE 窗口
select date_format(now(), 'yyyy-MM-dd HH:mm:ss')
     ,date_format(window_start, 'yyyy-MM-dd HH:mm:ss') AS wStart
     ,date_format(window_end, 'yyyy-MM-dd HH:mm:ss') AS wEnd
     ,count(user_id) pv
     ,count(distinct user_id) uv
FROM TABLE(
             TUMBLE(TABLE user_log, DESCRIPTOR(proc_time), INTERVAL '1' MINUTES )) t1
group by window_start, window_end
;

-- select date_format(now(), 'yyyy-MM-dd HH:mm:ss')
--      ,date_format(window_start, 'yyyy-MM-dd HH:mm:ss') AS wStart
--      ,date_format(window_end, 'yyyy-MM-dd HH:mm:ss') AS wEnd
--      ,count(user_id) pv
--      ,count(distinct user_id) uv
-- FROM TABLE(
--              CUMULATE(TABLE user_log, DESCRIPTOR(ts), INTERVAL '5' SECOND, INTERVAL '5' MINUTE)) t1
-- group by window_start, window_end
;

Windowing TVF 源码

先来个任务流图:

可以看到和 Group window 明显不同,windowing TVF 是结合 LocalWindowAggregate 和 GlobalWindowAggregate 实现

ExecGraph

第一次写这部分的源码解析,跳过 sql 解析部分,直接到 生成 execGraph 开始

PlannerBase.translate

override def translate(
      modifyOperations: util.List[ModifyOperation]): util.List[Transformation[_]] = {
    beforeTranslation()
    if (modifyOperations.isEmpty) {
      return List.empty[Transformation[_]]
    }

    val relNodes = modifyOperations.map(translateToRel)
    // sql 节点优化
    val optimizedRelNodes = optimize(relNodes)
    // 生成 exec graph
    val execGraph = translateToExecNodeGraph(optimizedRelNodes)
    // 提交到远程执行
    val transformations = translateToPlan(execGraph)
    afterTranslation()
    transformations
  }

看过相关介绍、博客的同学应该都知道,Flink 的执行图是从 sink 开始构建的,从 sink 节点开始,然后根据 sink 的输入边,构建 sink 的上游节点, 这样依次构建到 source 节点

那我们就从 StreamPhysicalSink 开始构建 sql 对应的执行图

PlannerBasetranslateToExecNodeGraph

 /**
    Converts FlinkPhysicalRel DAG to ExecNodeGraph, tries to reuse duplicate sub-plans and transforms the graph based on the given processors.
 */
 private[flink] def translateToExecNodeGraph(optimizedRelNodes: Seq[RelNode]): ExecNodeGraph = {
    val nonPhysicalRel = optimizedRelNodes.filterNot(_.isInstanceOf[FlinkPhysicalRel])
    if (nonPhysicalRel.nonEmpty) {
      throw new TableException("The expected optimized plan is FlinkPhysicalRel plan, " +
        s"actual plan is ${nonPhysicalRel.head.getClass.getSimpleName} plan.")
    }

    require(optimizedRelNodes.forall(_.isInstanceOf[FlinkPhysicalRel]))
    // Rewrite same rel object to different rel objects
    // in order to get the correct dag (dag reuse is based on object not digest)
    val shuttle = new SameRelObjectShuttle()
    val relsWithoutSameObj = optimizedRelNodes.map(_.accept(shuttle))
    // reuse subplan
    val reusedPlan = SubplanReuser.reuseDuplicatedSubplan(relsWithoutSameObj, tableConfig)
    // convert FlinkPhysicalRel DAG to ExecNodeGraph
    val generator = new ExecNodeGraphGenerator()
    // 调用 ExecNodeGraphGenerator 生成 执行图
    val execGraph = generator.generate(reusedPlan.map(_.asInstanceOf[FlinkPhysicalRel]))

    // process the graph
    val context = new ProcessorContext(this)
    val processors = getExecNodeGraphProcessors
    processors.foldLeft(execGraph)((graph, processor) => processor.process(graph, context))
  }

ExecNodeGraphGenerator.generate, 输入参数 List relNodes 就是 任务的 sink 节点的列表, 如果有多个 sink 会依次生成每个 sink 对应的流图, 生成后 添加到 rootNodes 列表中

在这里我们仔细看下任务流图的算子和上下游关系:

Source --> Calc --> LocalWindowAggregate --> GlobalWindowAggregate --> Calc --> Sink

所以在生成执行图时,就是反过来的:

Sink --> Calc --> GlobalWindowAggregate --> LocalWindowAggregate --> Calc --> Source

ExecNodeGraphGenerator.generate

public ExecNodeGraph generate(List<FlinkPhysicalRel> relNodes) {
    List<ExecNode<?>> rootNodes = new ArrayList<>(relNodes.size());
    for (FlinkPhysicalRel relNode : relNodes) {
        rootNodes.add(generate(relNode));
    }
    return new ExecNodeGraph(rootNodes);
}

ExecNodeGraphGenerator.generate

private ExecNode<?> generate(FlinkPhysicalRel rel) {
        // 构建之前先判断一下 节点是否已经构建过了(多sink 任务,部分算子有两个输出)
        ExecNode<?> execNode = visitedRels.get(rel);
        if (execNode != null) {
            return execNode;
        }

        if (rel instanceof CommonIntermediateTableScan) {
            throw new TableException("Intermediate RelNode can't be converted to ExecNode.");
        }

        List<ExecNode<?>> inputNodes = new ArrayList<>();
        // 获取当前节点的所有输入边,递归调用 generate 方法,构建上游节点,依次递归直达 source 节点(source 节点没有输入边)
        for (RelNode input : rel.getInputs()) {
            inputNodes.add(generate((FlinkPhysicalRel) input));
        }

        // 构建节点: 调用节点对应的 translateToExecNode 初始化 节点
        execNode = rel.translateToExecNode();

        // 添加输入边
        // connects the input nodes
        List<ExecEdge> inputEdges = new ArrayList<>(inputNodes.size());
        for (ExecNode<?> inputNode : inputNodes) {
            inputEdges.add(ExecEdge.builder().source(inputNode).target(execNode).build());
        }
        execNode.setInputEdges(inputEdges);

        // 记录所有构建的节点
        visitedRels.put(rel, execNode);
        return execNode;
    }

6 层递归,依次创建所有算子: 

列举一下: StreamPhysicalLocalWindowAggregate、StreamPhysicalGlobalWindowAggregate

StreamPhysicalLocalWindowAggregate.translateToExecNode

override def translateToExecNode(): ExecNode[_] = {
    checkEmitConfiguration(FlinkRelOptUtil.getTableConfigFromContext(this))
    new StreamExecLocalWindowAggregate(
      unwrapTableConfig(this),
      grouping,
      aggCalls.toArray,
      windowing,
      InputProperty.DEFAULT,
      FlinkTypeFactory.toLogicalRowType(getRowType),
      getRelDetailedDescription)
  }

StreamPhysicalGlobalWindowAggregate.translateToExecNode

override def translateToExecNode(): ExecNode[_] = {
    checkEmitConfiguration(FlinkRelOptUtil.getTableConfigFromContext(this))
    new StreamExecGlobalWindowAggregate(
      unwrapTableConfig(this),
      grouping,
      aggCalls.toArray,
      windowing,
      namedWindowProperties.toArray,
      InputProperty.DEFAULT,
      FlinkTypeFactory.toLogicalRowType(inputRowTypeOfLocalAgg),
      FlinkTypeFactory.toLogicalRowType(getRowType),
      getRelDetailedDescription)
  }

生成执行图后就调用 translateToPlan 提交任务到 TM 执行

PlannerBase.translate

override def translate(
      modifyOperations: util.List[ModifyOperation]): util.List[Transformation[_]] = {
    beforeTranslation()
    if (modifyOperations.isEmpty) {
      return List.empty[Transformation[_]]
    }

    val relNodes = modifyOperations.map(translateToRel)
    // sql 节点优化
    val optimizedRelNodes = optimize(relNodes)
    // 生成 exec graph
    val execGraph = translateToExecNodeGraph(optimizedRelNodes)
    // 提交到远程执行
    val transformations = translateToPlan(execGraph)
    afterTranslation()
    transformations
  }

然后又是递归,依次调用每个节点的 translateToPlanInternal 方法,创建算子实例

比如 source:

CommonExecTableSourceScan.translateToPlanInternal

@Override
protected Transformation<RowData> translateToPlanInternal(
        PlannerBase planner, ExecNodeConfig config) {
        
    final ScanTableSource tableSource =
            tableSourceSpec.getScanTableSource(planner.getFlinkContext());
    ScanTableSource.ScanRuntimeProvider provider =
            tableSource.getScanRuntimeProvider(ScanRuntimeProviderContext.INSTANCE);
    if (provider instanceof SourceFunctionProvider) {
        final SourceFunctionProvider sourceFunctionProvider = (SourceFunctionProvider) provider;
        final SourceFunction<RowData> function = sourceFunctionProvider.createSourceFunction();
        final Transformation<RowData> transformation =
                createSourceFunctionTransformation(
                        env,
                        function,
                        sourceFunctionProvider.isBounded(),
                        meta.getName(),
                        outputTypeInfo);
        return meta.fill(transformation);
   
   。。。。
}

这里多一重递归是算子 local 和 globa 算子是断开的,涉及数据分区策略

然后我们终于进到 windowing TVF 具体的代码上了

GlobalWindowAggregate 源码

StreamExecGlobalWindowAggregate.translateToPlanInternal


 @Override
protected Transformation<RowData> translateToPlanInternal(
        PlannerBase planner, ExecNodeConfig config) {
    final ExecEdge inputEdge = getInputEdges().get(0);
    final Transformation<RowData> inputTransform =
            (Transformation<RowData>) inputEdge.translateToPlan(planner);
    // 获取上游算子输出数据类型,即当前算子输入数据类型
    final RowType inputRowType = (RowType) inputEdge.getOutputType();

    // 获取时区
    final ZoneId shiftTimeZone =
            TimeWindowUtil.getShiftTimeZone(
                    windowing.getTimeAttributeType(),
                    TableConfigUtils.getLocalTimeZone(config));
    // 创建 ”分片分配器“,类似于 窗口分配器
    final SliceAssigner sliceAssigner = createSliceAssigner(windowing, shiftTimeZone);

    // local agg 方法列表
    final AggregateInfoList localAggInfoList =
            AggregateUtil.deriveStreamWindowAggregateInfoList(
                    localAggInputRowType, // should use original input here
                    JavaScalaConversionUtil.toScala(Arrays.asList(aggCalls)),
                    windowing.getWindow(),
                    false); // isStateBackendDataViews
    // global agg 方法列表
    final AggregateInfoList globalAggInfoList =
            AggregateUtil.deriveStreamWindowAggregateInfoList(
                    localAggInputRowType, // should use original input here
                    JavaScalaConversionUtil.toScala(Arrays.asList(aggCalls)),
                    windowing.getWindow(),
                    true); // isStateBackendDataViews

    // 生成 local agg function 代码, 
    // handler used to merge multiple local accumulators into one accumulator,用于将多个本地累加器合并为一个累加器的处理程序,
    // where the accumulators are all on memory,累加器都在内存中
    final GeneratedNamespaceAggsHandleFunction<Long> localAggsHandler =
            createAggsHandler(
                    "LocalWindowAggsHandler",
                    sliceAssigner,
                    localAggInfoList,
                    grouping.length,
                    true,
                    localAggInfoList.getAccTypes(),
                    config,
                    planner.getRelBuilder(),
                    shiftTimeZone);

    // 生成 global agg function 代码
    // handler used to merge the single local accumulator (on memory) into state accumulator
    // 用于将单个本地累加器(在内存上)合并到状态累加器的处理程序
    final GeneratedNamespaceAggsHandleFunction<Long> globalAggsHandler =
            createAggsHandler(
                    "GlobalWindowAggsHandler",
                    sliceAssigner,
                    globalAggInfoList,
                    0,
                    true,
                    localAggInfoList.getAccTypes(),
                    config,
                    planner.getRelBuilder(),
                    shiftTimeZone);

    // handler used to merge state accumulators for merging slices into window,
    // 用于合并状态累加器以将切片合并到窗口中的处理程序,
    // e.g. Hop and Cumulate
    final GeneratedNamespaceAggsHandleFunction<Long> stateAggsHandler =
            createAggsHandler(
                    "StateWindowAggsHandler",
                    sliceAssigner,
                    globalAggInfoList,
                    0,
                    false,
                    globalAggInfoList.getAccTypes(),
                    config,
                    planner.getRelBuilder(),
                    shiftTimeZone);

    final RowDataKeySelector selector =
            KeySelectorUtil.getRowDataSelector(grouping, InternalTypeInfo.of(inputRowType));
    final LogicalType[] accTypes = convertToLogicalTypes(globalAggInfoList.getAccTypes());
    // 生成窗口算子 SlicingWindowOperator,又到了熟悉的地方了,类似于 WindowOperator 的算子
    final OneInputStreamOperator<RowData, RowData> windowOperator =
            SlicingWindowAggOperatorBuilder.builder()
                    .inputSerializer(new RowDataSerializer(inputRowType))
                    .shiftTimeZone(shiftTimeZone)
                    .keySerializer(
                            (PagedTypeSerializer<RowData>)
                                    selector.getProducedType().toSerializer())
                    .assigner(sliceAssigner)
                    .countStarIndex(globalAggInfoList.getIndexOfCountStar())
                    .globalAggregate(
                            localAggsHandler,
                            globalAggsHandler,
                            stateAggsHandler,
                            new RowDataSerializer(accTypes))
                    .build();
    // 生成 算子的 transform
    final OneInputTransformation<RowData, RowData> transform =
            ExecNodeUtil.createOneInputTransformation(
                    inputTransform,
                    createTransformationMeta(GLOBAL_WINDOW_AGGREGATE_TRANSFORMATION, config),
                    SimpleOperatorFactory.of(windowOperator),
                    InternalTypeInfo.of(getOutputType()),
                    inputTransform.getParallelism(),
                    WINDOW_AGG_MEMORY_RATIO);

    // set KeyType and Selector for state
    transform.setStateKeySelector(selector);
    transform.setStateKeyType(selector.getProducedType());
    return transform;
}

agg info 列表:

SlicingWindowOperator:

StreamExecWindowAggregateBase.createSliceAssigner


protected SliceAssigner createSliceAssigner(
        WindowSpec windowSpec, int timeAttributeIndex, ZoneId shiftTimeZone) {
    // 基于不同窗口类型,创建对应的 分片分配器

    // tumble
    if (windowSpec instanceof TumblingWindowSpec) {
        Duration size = ((TumblingWindowSpec) windowSpec).getSize();
        SliceAssigners.TumblingSliceAssigner assigner =
                SliceAssigners.tumbling(timeAttributeIndex, shiftTimeZone, size);
        Duration offset = ((TumblingWindowSpec) windowSpec).getOffset();
        if (offset != null) {
            assigner = assigner.withOffset(offset);
        }
        return assigner;

        // hop
    } else if (windowSpec instanceof HoppingWindowSpec) {
        Duration size = ((HoppingWindowSpec) windowSpec).getSize();
        Duration slide = ((HoppingWindowSpec) windowSpec).getSlide();
        if (size.toMillis() % slide.toMillis() != 0) {
            throw new TableException(
                    String.format(
                            "HOP table function based aggregate requires size must be an "
                                    + "integral multiple of slide, but got size %s ms and slide %s ms",
                            size.toMillis(), slide.toMillis()));
        }
        SliceAssigners.HoppingSliceAssigner assigner =
                SliceAssigners.hopping(timeAttributeIndex, shiftTimeZone, size, slide);
        Duration offset = ((HoppingWindowSpec) windowSpec).getOffset();
        if (offset != null) {
            assigner = assigner.withOffset(offset);
        }
        return assigner;

        // cumulate
    } else if (windowSpec instanceof CumulativeWindowSpec) {
        Duration maxSize = ((CumulativeWindowSpec) windowSpec).getMaxSize();
        Duration step = ((CumulativeWindowSpec) windowSpec).getStep();
        if (maxSize.toMillis() % step.toMillis() != 0) {
            throw new TableException(
                    String.format(
                            "CUMULATE table function based aggregate requires maxSize must be an "
                                    + "integral multiple of step, but got maxSize %s ms and step %s ms",
                            maxSize.toMillis(), step.toMillis()));
        }
        SliceAssigners.CumulativeSliceAssigner assigner =
                SliceAssigners.cumulative(timeAttributeIndex, shiftTimeZone, maxSize, step);
        Duration offset = ((CumulativeWindowSpec) windowSpec).getOffset();
        if (offset != null) {
            assigner = assigner.withOffset(offset);
        }
        return assigner;

        // 不支持其他
    } else {
        throw new UnsupportedOperationException(windowSpec + " is not supported yet.");
    }
}

翻滚窗口:

SlicingWindowOperator 通过 onTimer 事件来触发窗口计算:

SlicingWindowOperator.onTimer

private void onTimer(InternalTimer<K, W> timer) throws Exception {
    setCurrentKey(timer.getKey());
    W window = timer.getNamespace();
    // 触发窗口计算
    windowProcessor.fireWindow(window);
    // 清理完成的窗口
    windowProcessor.clearWindow(window);
    // we don't need to clear window timers,
    // because there should only be one timer for each window now, which is current timer.
}

SliceUnsharedWindowAggProcessor.fireWindow: 通过获取窗口累加器,获取累加器的结果,输出数据

@Override
public void fireWindow(Long windowEnd) throws Exception {
    RowData acc = windowState.value(windowEnd);
    if (acc == null) {
        acc = aggregator.createAccumulators();
    }
    aggregator.setAccumulators(windowEnd, acc);
    RowData aggResult = aggregator.getValue(windowEnd);
    collect(aggResult);
}

然后 GlobalWindowAggregate 部分就完成了

LocalWindowAggregate 源码

LocalWindowAggregate 部分的源码,比 GlobalWindowAggregate 部分的简单很多,一样有 sliceAssigner/ aggInfoList/generatedAggsHandler, 然后再接一个简单的 LocalSlicingWindowAggOperator

StreamExecLocalWindowAggregate.translateToPlanInternal

protected Transformation<RowData> translateToPlanInternal(
        PlannerBase planner, ExecNodeConfig config) {
    final ExecEdge inputEdge = getInputEdges().get(0);
    final Transformation<RowData> inputTransform =
            (Transformation<RowData>) inputEdge.translateToPlan(planner);
    final RowType inputRowType = (RowType) inputEdge.getOutputType();
    // 时区
    final ZoneId shiftTimeZone =
            TimeWindowUtil.getShiftTimeZone(
                    windowing.getTimeAttributeType(),
                    TableConfigUtils.getLocalTimeZone(config));
    // 分片分配器
    final SliceAssigner sliceAssigner = createSliceAssigner(windowing, shiftTimeZone);
    // agg 方法列表
    final AggregateInfoList aggInfoList =
            AggregateUtil.deriveStreamWindowAggregateInfoList(
                    inputRowType,
                    JavaScalaConversionUtil.toScala(Arrays.asList(aggCalls)),
                    windowing.getWindow(),
                    false); // isStateBackendDataViews
    // 生成的代码
    final GeneratedNamespaceAggsHandleFunction<Long> generatedAggsHandler =
            createAggsHandler(
                    sliceAssigner,
                    aggInfoList,
                    config,
                    planner.getRelBuilder(),
                    inputRowType.getChildren(),
                    shiftTimeZone);
    final RowDataKeySelector selector =
            KeySelectorUtil.getRowDataSelector(grouping, InternalTypeInfo.of(inputRowType));

    PagedTypeSerializer<RowData> keySer =
            (PagedTypeSerializer<RowData>) selector.getProducedType().toSerializer();
    AbstractRowDataSerializer<RowData> valueSer = new RowDataSerializer(inputRowType);

    WindowBuffer.LocalFactory bufferFactory =
            new RecordsWindowBuffer.LocalFactory(
                    keySer, valueSer, new LocalAggCombiner.Factory(generatedAggsHandler));
    // 窗口算子
    final OneInputStreamOperator<RowData, RowData> localAggOperator =
            new LocalSlicingWindowAggOperator(
                    selector, sliceAssigner, bufferFactory, shiftTimeZone);

    return ExecNodeUtil.createOneInputTransformation(
            inputTransform,
            createTransformationMeta(LOCAL_WINDOW_AGGREGATE_TRANSFORMATION, config),
            SimpleOperatorFactory.of(localAggOperator),
            InternalTypeInfo.of(getOutputType()),
            inputTransform.getParallelism(),
            // use less memory here to let the chained head operator can have more memory
            WINDOW_AGG_MEMORY_RATIO / 2);
}

数据的处理流程也比较简单:

  1. LocalSlicingWindowAggOperator 处理数据输入:

LocalSlicingWindowAggOperator.processElement 获取数据属于的窗口分片(已分片结尾时间命名),将数据暂存到 windowBuffer

@Override
    public void processElement(StreamRecord<RowData> element) throws Exception {
        RowData inputRow = element.getValue();
        RowData key = keySelector.getKey(inputRow);
        long sliceEnd = sliceAssigner.assignSliceEnd(inputRow, CLOCK_SERVICE);
        // may flush to output if buffer is full
        windowBuffer.addElement(key, sliceEnd, inputRow);
    }

RecordsWindowBuffer.addElement


@Override
public void addElement(RowData key, long sliceEnd, RowData element) throws Exception {
    // track the lowest trigger time, if watermark exceeds the trigger time,
    // it means there are some elements in the buffer belong to a window going to be fired,
    // and we need to flush the buffer into state for firing.
    minSliceEnd = Math.min(sliceEnd, minSliceEnd);

    reuseWindowKey.replace(sliceEnd, key);
    LookupInfo<WindowKey, Iterator<RowData>> lookup = recordsBuffer.lookup(reuseWindowKey);
    try {
        recordsBuffer.append(lookup, recordSerializer.toBinaryRow(element));
    } catch (EOFException e) {
        // buffer is full, flush it to state
        flush();
        // remember to add the input element again
        addElement(key, sliceEnd, element);
    }
}

watermark 触发 LocalSlicingWindowAggOperator.processWatermark 根据 watermark 和窗口时间,触发窗口

LocalSlicingWindowAggOperator.processWatermark

@Override
public void processWatermark(Watermark mark) throws Exception {
    if (mark.getTimestamp() > currentWatermark) {
        currentWatermark = mark.getTimestamp();
        if (currentWatermark >= nextTriggerWatermark) {
            // we only need to call advanceProgress() when current watermark may trigger window
            // 创发窗口
            windowBuffer.advanceProgress(currentWatermark);
            nextTriggerWatermark =
                    getNextTriggerWatermark(
                            currentWatermark, windowInterval, shiftTimezone, useDayLightSaving);
        }
    }
    super.processWatermark(mark);
}

RecordsWindowBuffer.advanceProgress 判断是否触发窗口

@Override
public void advanceProgress(long progress) throws Exception {
    if (isWindowFired(minSliceEnd, progress, shiftTimeZone)) {
        // there should be some window to be fired, flush buffer to state first
        flush();
    }
}

RecordsWindowBuffer.isWindowFired

public static boolean isWindowFired(
        long windowEnd, long currentProgress, ZoneId shiftTimeZone) {
    // Long.MAX_VALUE is a flag of min window end, directly return false
    if (windowEnd == Long.MAX_VALUE) {
        return false;
    }
    long windowTriggerTime = toEpochMillsForTimer(windowEnd - 1, shiftTimeZone);
    // 当前时间大于窗口触发时间
    return currentProgress >= windowTriggerTime;
}

触发窗口计算是调用 flush , 调用 combineFunction 先合并每个 key 本地数据

RecordsWindowBuffer.flush

@Override
public void flush() throws Exception {
    if (recordsBuffer.getNumKeys() > 0) {
        KeyValueIterator<WindowKey, Iterator<RowData>> entryIterator =
                recordsBuffer.getEntryIterator(requiresCopy);
        while (entryIterator.advanceNext()) {
            combineFunction.combine(entryIterator.getKey(), entryIterator.getValue());
        }
        recordsBuffer.reset();
        // reset trigger time
        minSliceEnd = Long.MAX_VALUE;
    }

LocalAggCombiner.combine,执行 agg 流程: 创建累加器,执行累加,最后输出数据到下游

 @Override
public void combine(WindowKey windowKey, Iterator<RowData> records) throws Exception {
    // always not copy key/value because they are not cached.
    final RowData key = windowKey.getKey();
    final Long window = windowKey.getWindow();

    // step 1: create an empty accumulator
    RowData acc = aggregator.createAccumulators();

    // step 2: set accumulator to function
    aggregator.setAccumulators(window, acc);

    // step 3: do accumulate
    while (records.hasNext()) {
        RowData record = records.next();
        if (isAccumulateMsg(record)) {
            aggregator.accumulate(record);
        } else {
            aggregator.retract(record);
        }
    }

    // step 4: get accumulator and output accumulator
    acc = aggregator.getAccumulators();
    output(key, window, acc);
}

LocalWindowAggregate 搞定

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 快速傅里叶变换(FFT)是一种在信号处理和数据分析中广泛使用的算法。FFT可以将时间域上的信号转换为频域上的信号,从而更好地理解信号的频率和特征。FFT比传统的傅里叶变换更快,并且可以在实际应用中高效地处理大量数据。 为了正确使用FFT,需要了解窗函数(windowing)的概念。窗函数可以将信号从时间域转换到频域时消除信号中存在的噪声和谐波。窗函数通常是一些标准函数,比如汉宁(Hanning),汉明(Hamming)和布莱克曼(Blackman)窗,可以通过乘以信号进行应用。 使用窗函数的目标是通过克服FFT从正弦波生产的边缘效应来减少泄漏。泄漏是指输入信号中存在的特定频率信号误被认为是分析信号中不同频率信号的影响。这个特定频率信号可以是信号的本征幅度中的噪声或谐波,而不是真正的要分析的信号分量。 简而言之,理解FFT和窗函数非常重要,可以帮助我们更好地处理信号并获得准确的频域特征,以便更好地理解并优化各种应用。 ### 回答2: 傅里叶变换(FFT)是一种将时域信号变换到频域的方法,它是数字信号处理中最常用的技术之一。理解FFT的过程中需要掌握如何进行离散傅里叶变换,也需要了解FFT算法是如何通过减少计算复杂度来优化离散傅里叶变换的。 在计算FFT前,通常需要对信号进行windowing处理,以减少频谱泄漏的影响。这是因为离散傅里叶变换假设信号有着周期的重复性,但实际的信号往往是非周期性的。因此,如果直接对整段信号进行FFT,会导致频谱泄漏,使得分析结果不准确。通过窗口函数将信号分成多个窗口并进行FFT,可以有效减少频谱泄漏的影响,提高分析结果的精度。 在使用窗口函数时,需要根据需要选择适当的窗口函数类型和参数。常见的窗口函数包括汉宁窗、汉明窗、布莱克曼窗等。根据不同窗口函数的性质,可以选择合适的窗口函数来适应不同的信号类型和分析需求,以获得更准确的分析结果。 总之,理解FFT和windowing是数字信号处理的基本技术,掌握这些技术可以帮助我们更好地理解和分析信号,为信号处理和应用提供更好的技术支持。 ### 回答3: FFTs和Windowing是数字信号处理中两个非常重要的概念。 FFT是一种将时间域信号转换为频域信号的技术。它利用了傅里叶变换的原理,将信号从时间域的连续函数转换为频域的复数数组,这个数组的每个元素代表了该频域上该频率成分的强度。因为它能够将信号的频域特性可视化,所以其在信号处理中使用非常广泛。 然而在进行FFT分析时,需要将原始信号在边界处附加零值,以防止频谱泄露和溢出。这就要用到Windowing技术,它是将原始数据窗化后再进行FFT分析的过程。Window的设计需要在时间域和频域上都保持适当的平滑性和截止特性,以便将周期存在的信号能够被FFT捕捉,同时避免无用的频率成分被噪声干扰。常见的Window函数有Hamming窗、Blackman窗等。 综上所述,FFT和Windowing这两个概念在数字信号处理中不可分割,能够进行FFT分析的Windowing实现是计算信号频谱分析的重要工具。在实际应用中,对于不同的信号和特定的分析方法,需要仔细考虑FFT和Windowing的选择和设计,以获得最佳的结果。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值