Thanks to visit codestin.com
Credit goes to github.com

Skip to content

AddPictureFromBytes becomes O(n²) when inserting many distinct images — a recurring "recompute via full scan" pattern across countDrawings/addMedia/countMedia/relsReader/addRels #2393

Description

@leejongkyoo1984

Description

Inserting N distinct images into a single sheet via AddPictureFromBytes in a loop scales roughly O(n²), not O(n). There are (at least) four separate places on this call path that do a linear sync.Map.Range() scan over state that grows with n, on every single call:

  • countDrawings() — called unconditionally at the top of AddPictureFromBytes (drawingID := f.countDrawings() + 1) to compute a candidate drawingID, but that value is discarded and recomputed from the existing relationship whenever the worksheet already has a drawing (ws.Drawing != nil branch inside prepareDrawing()) — i.e. on every call after the first picture on a given sheet. Confirmed present, unchanged, in v2.9.1 through v2.11.0 (latest).
  • addMedia()'s duplicate-image check (f.Pkg.Range + bytes.Equal against every stored media) — a hash-indexed approach for this (noted in Saving Duplicate Images #359) turns out to fix the bulk-insert case well.
  • countMedia(), called from addMedia(), re-scans all of f.Pkg just to compute the next numeric media filename.
  • the relsReader(drawingRels) relationship-dedup scan inside AddPictureFromBytes itself.

Steps to reproduce

package main

import (
	"fmt"
	"github.com/xuri/excelize/v2"
)

func main() {
	f := excelize.NewFile()
	for i := 0; i < 25000; i++ {
		img := makeDistinctPNGBytes(i) // any valid PNG, must differ per i
		cell := fmt.Sprintf("A%d", i+1)
		f.SetRowHeight("Sheet1", i+1, 160)
		_ = f.AddPictureFromBytes("Sheet1", cell, &excelize.Picture{
			Extension: ".png", File: img,
			Format: &excelize.GraphicOptions{AutoFit: true, ScaleX: 0.98, ScaleY: 0.98},
		})
	}
	_ = f.SaveAs("out.xlsx")
}

Results observed (n=25,000, Apple M-series, embed loop only — image generation excluded):

State Time
Unpatched v2.9.1 ~38–41s
+ fix relsReader scan, addMedia dedup scan, countMedia scan ~24.3s
+ additionally skip the redundant countDrawings() call ~8.3s (~4.7–4.9x total)

Per-call latency also confirms superlinear growth in the unpatched version — e.g. call #1 ≈ 26µs vs call #25,000 ≈ 2.7ms (~100x), consistent with O(n) cost per call rather than O(1).

Results expected: near-linear total scaling; each AddPictureFromBytes call shouldn't need to re-scan state proportional to how many pictures/media/drawings already exist.

Fix: I have a local patch (hash-bucket index for media dedup, an atomic counter replacing countMedia()'s scan, an indexed lookup replacing the relationship-dedup scan, and skipping the countDrawings() call when the worksheet already has a drawing) that produces the ~8.3s result above, verified for correctness (byte-identical round-trip for first/last images, no dedup false-positives between distinct images). Happy to open a PR if useful.

A fifth, related instance in addRels()

While profiling the patched version, addRels() (excelize.go) became the new dominant cost — it does a full scan on every call to compute the next unused relationship ID, instead of maintaining a running counter. This one is called from 23 sites across 12 files (chart.go, picture.go, rows.go, drawing.go, pivotTable.go, sheet.go, excelize.go, shape.go, slicer.go, stream.go, table.go, vml.go), so fixing it would speed up bulk operations across charts, pivot tables, slicers, tables, comments, and hyperlinks too — not just image embedding. Happy to help draft a patch for this one as well if there's interest.

This looks like a broader pattern, not five isolated bugs

Across countDrawings(), addMedia(), countMedia(), the relsReader dedup loop, and addRels(), the same shape keeps recurring: a derived value (a count, a max ID, a dedup check) is recomputed via a full linear scan on every call instead of being maintained incrementally. A structural fix would add a maintained counter/index to the shared types these all scan — most importantly xlsxRelationships in xmlWorkbook.go (for addRels()'s max-ID computation), plus equivalent bookkeeping for Pkg/Drawings. That would resolve not just the five call sites identified here but likely prevent the same class of issue anywhere else in the codebase that touches these shared structures during bulk operations.

Excelize version: v2.9.1 (confirmed the same code paths unchanged through v2.11.0, latest as of writing)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions