Repository: pingcap/tidb
File: pkg/statistics/handle/globalstats/global_stats_async.go (Go)

Code before:
	})
	err = a.loadHistogramAndTopN(sctx, a.globalTableInfo, isIndex)
	if err != nil {
		close(a.ioWorkerExitWhenErrChan)
		return err
	}
	close(a.histogramAndTopn)
	return nil
}

func (a *AsyncMergePartitionStats2GlobalStats) cpuWorker(stmtCtx *stmtctx.StatementContext, sctx sessionctx.Context, opts map[ast.AnalyzeOptionType]uint64, isIndex bool, tz *time.Location, analyzeVersion int) (err error) {
	defer func() {
		if r := recover(); r != nil {
			statslogutil.StatsLogger().Warn("cpuWorker panic", zap.Stack("stack"), zap.Any("error", r))
			err = errors.New(fmt.Sprint(r))
		}
		close(a.cpuWorkerExitChan)
	}()
	a.dealFMSketch()
	select {
	case <-a.ioWorkerExitWhenErrChan:
		return nil
	default:
		for i := range a.globalStats.Num {
			// Update the global NDV.
			globalStatsNDV := min(a.globalStats.Fms[i].NDV(), a.globalStats.Count)
			a.globalStatsNDV = append(a.globalStatsNDV, globalStatsNDV)
			a.globalStats.Fms[i] = nil
		}
	}
	err = a.dealCMSketch()
	if err != nil {
		statslogutil.StatsLogger().Warn("dealCMSketch failed", zap.Error(err))
		return err
	}
	failpoint.Inject("PanicSameTime", func(val failpoint.Value) {
		if val, _ := val.(bool); val {
			time.Sleep(1 * time.Second)
			panic("test for PanicSameTime")
		}
	})
	err = a.dealHistogramAndTopN(stmtCtx, sctx, opts, isIndex, tz, analyzeVersion)
	if err != nil {
		statslogutil.StatsLogger().Warn("dealHistogramAndTopN failed", zap.Error(err))
		return err
	}
	return nil
}

// Result returns the global stats.
func (a *AsyncMergePartitionStats2GlobalStats) Result() *GlobalStats {

Review comment (suggestion):
In cpuWorker, a.globalStats.Fms[i] can still be nil if loadFmsketch skipped all partitions for a given histID (e.g. when SkipMissingPartitionStats is enabled and every partition lacks that column/index stats). Calling a.globalStats.Fms[i].NDV() would panic. Please guard against nil here (e.g. treat NDV as 0 / skip this idx) or ensure dealFMSketch always initializes a.globalStats.Fms[i] before this loop.
```suggestion
			var globalStatsNDV uint64
			if a.globalStats.Fms[i] != nil {
				globalStatsNDV = min(a.globalStats.Fms[i].NDV(), a.globalStats.Count)
			}
```

Code after:
	})
	err = a.loadHistogramAndTopN(sctx, a.globalTableInfo, isIndex)
	if err != nil {
		close(a.ioWorkerExitWhenErrChan)
		return err
	}
	close(a.histogramAndTopn)
	return nil
}

func (a *AsyncMergePartitionStats2GlobalStats) cpuWorker(stmtCtx *stmtctx.StatementContext, sctx sessionctx.Context, opts map[ast.AnalyzeOptionType]uint64, isIndex bool, tz *time.Location, analyzeVersion int) (err error) {
	defer func() {
		if r := recover(); r != nil {
			statslogutil.StatsLogger().Warn("cpuWorker panic", zap.Stack("stack"), zap.Any("error", r))
			err = errors.New(fmt.Sprint(r))
		}
		close(a.cpuWorkerExitChan)
	}()
	a.dealFMSketch()
	select {
	case <-a.ioWorkerExitWhenErrChan:
		return nil
	default:
		for i := range a.globalStats.Num {
			// Update the global NDV.
			globalStatsNDV := min(a.globalStats.Fms[i].NDV(), a.globalStats.Count)
			a.globalStatsNDV = append(a.globalStatsNDV, globalStatsNDV)
			a.globalStats.Fms[i] = nil // Release for GC.
		}
	}
	err = a.dealCMSketch()
	if err != nil {
		statslogutil.StatsLogger().Warn("dealCMSketch failed", zap.Error(err))
		return err
	}
	failpoint.Inject("PanicSameTime", func(val failpoint.Value) {
		if val, _ := val.(bool); val {
			time.Sleep(1 * time.Second)
			panic("test for PanicSameTime")
		}
	})
	err = a.dealHistogramAndTopN(stmtCtx, sctx, opts, isIndex, tz, analyzeVersion)
	if err != nil {
		statslogutil.StatsLogger().Warn("dealHistogramAndTopN failed", zap.Error(err))
		return err
	}
	return nil
}

// Result returns the global stats.
func (a *AsyncMergePartitionStats2GlobalStats) Result() *GlobalStats {