Teknisk artikkel

Gjentatte ODS-rader som radhøyde-serier i HotXLS for Delphi

HotXLS Delphi Component lagrer en ODS-rad som bærer table:number-rows-repeated og en radhøyde som én enkelt TXLSXRowHeightRun-record — første rad, siste rad, én høyde — i stedet for én høydepost per gjentatte rad, og bretter stilen for tomme celler som de radene arver, inn i ett intervallstiloverlegg. Det er hele grunnen til at HotXLS 2.382.2 åpner et regneark der halen gjentar 1,048,530 tomme rader på 0.02 sekunder der 2.382.1 gikk i tidsavbrudd, og til at den samme filen lagres tilbake til ODS med gjentakelsestallet intakt i stedet for som en million bokstavelige rader

Filen det gjelder, er helt vanlig. LibreOffice Calc skriver et ark med fjorten kolonner og 45 rader med data, og beskriver så alt nedenfor dem med ett enkelt element: <table:table-row table:style-name="ro1" table:number-rows-repeated="1048530"><table:table-cell table:number-columns-repeated="14"/></table:table-row>. Stilen ro1 setter style:row-height="0.452cm", og hver <table:table-column> bærer en table:default-cell-style-name som hver tom celle i serien arver. Hele content.xml er 103 KB. Ingenting ved filen sier «dyr»; kostnaden var utelukkende vår

Hvordan HotXLS gjør én gjentatt ODS-rad om til kompakt tilstand: elementet i content.xml med table:number-rows-repeated 1048530 og stil ro1 avbildes til én enkelt TXLSXRowHeightRun-record som spenner over rad 46 til 1048575 med 12.81 pt, pluss én StyleOverlays-oppføring per kolonne, mens versjon 2.382.1 utvidet det samme elementet til en million SetRowHeight-oppføringer og celleobjekter
Gjentakelsestallet, radhøyden i ro1 og standardstilene for kolonnene beskriver hver tom rad nedenfor rad 45, så importøren kan bygge én serie-record og overlegg per kolonne uten å røre en million koordinater

Hvorfor gir én gjentatt rad tidsavbrudd i en ODS-import?

Fordi importøren pleide å utvide den. I 2.382.1 løp radavslutteren SetRowHeight(RowIndex + i, RowHeight) én gang per gjentatte rad og skrev hver høyde inn i en Name=Value-strengliste med radnummer som nøkkel. Hver innsetting i den listen kjørte et IndexOfName-oppslag over alt som allerede lå der, så en million høyder kostet en million lineære søk — det kvadratiske listesøket HXLS-005 ble rapportert mot. Samtidig materialiserte OdsCommitRow et celleobjekt for hver kolonne som arvet en stil, på hver eneste av de gjentatte radene, fordi en stilisert tom celle fortsatt tellet som en celle

Lagringssiden hadde sin egen versjon av problemet. LibreOffice-filen slutter med én ekstra ro1-rad etter den store gjentakelsen, så den høyeste stilerte raden lå helt nederst i arket, og OdsBuildTableXml gikk gjennom hver rad opp til den og sendte ut <table:table-row>-elementer ett om gangen. Selv en arbeidsbok som var importert billig, ville ha blitt skrevet dyrt. Å rette importen uten å rette eksporten ville ha flyttet tidsavbruddet, ikke fjernet det

Hva er en radhøyde-serie i HotXLS?

En serie er det minste som kan beskrive «rad 46 til 1,048,575 er alle 12.81 punkter høye» uten å si det 1,048,530 ganger. TXLSXRowHeightRun er en record med FirstRow, LastRow og Height; TXLSXRowHeightRuns er et dynamisk array av dem, og hvert TXLSXWorksheet holder ett i FRowHeightRuns ved siden av den eksisterende listen over høyder per rad. Ved ODS-import forgrener radavslutteren seg nå på gjentakelsestallet: tallet 1 kaller fortsatt SetRowHeight, alt større kaller XlsxAssignRowHeightRun én gang for hele spennet. Spennet klippes til XlsxMaxRow, som er 1,048,576, så et gjentakelsestall som skyter over arket, trunkeres i stedet for å avvises

XlsxAssignRowHeightRun er den eneste skriveren til arrayet, og den holder seriene usammenfallende ved konstruksjon. Gitt et nytt intervall kopierer den hver eksisterende serie som ligger helt utenfor det, deler enhver serie som overlapper det, i stykket før og stykket etter, og legger så til det nye intervallet når Present er true — eller legger til ingenting når Present er false, som er måten ClearRowHeight slår hull på én rad. To ting følger. Arrayet inneholder aldri overlappende intervaller, så et oppslag kan stoppe ved første treff. Og arrayet muteres aldri på stedet; en fersk kopi bygges ved hvert kall, noe som ikke koster noe i de størrelsene det gjelder, og som fjerner en hel klasse aliaseringsfeil

var
  Workbook: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
begin
  Workbook := TXLSXWorkbook.Create;
  try
    // Et ark der haleraden gjentas 1,048,530 ganger under én radstil
    Workbook.OpenODS('conditional-formatting.ods');
    Sheet := Workbook.Sheets[1];
    // Begge lesningene løses gjennom den samme serien; ingenting ble utvidet
    Writeln(Sheet.RowHeight[46]:0:2, ' pt');
    Writeln(Sheet.RowHeight[1048575]:0:2, ' pt');
    // En overstyring på én rad skygger for serien uten å dele den
    Sheet.RowHeight[500000] := 36;
    // Å tømme én rad inne i serien deler serien i to stykker
    Sheet.ClearRowHeight(500001);
    Writeln(Sheet.HasRowHeight(500001)); // False
    Writeln(Sheet.RowHeight[500002]:0:2, ' pt'); // fortsatt seriehøyden
  finally
    Workbook.Free;
  end;
end;

Oppslagsrekkefølgen er den delen som er verdt å pugge. TXLSXWorksheet.GetRowHeight sjekker listen per rad først og konsulterer seriene bare når raden ikke har noen eksplisitt oppføring, og HasRowHeight gjør det samme. Så Sheet.RowHeight[500000] := 36 rører ikke serien i det hele tatt — den legger til én oppføring i listen per rad, og den oppføringen vinner fordi den slås opp først. ClearRowHeight er det motsatte: den fjerner enhver oppføring per rad og kaller deretter XlsxAssignRowHeightRun med Present = False, fordi en tømt rad må leses som «ingen høyde» selv om en serie dekker den. ClearRowHeights tømmer begge strukturene på én gang

Kirurgi på radhøyde-serier i HotXLS: etter OpenODS dekker én serie rad 46 til 1048575 med 12.81 pt, mens en oppføring per rad setter rad 500000 til 36 pt og vinner oppslaget fordi GetRowHeight sjekker listen per rad først, og ClearRowHeight på rad 500001 deler serien i to usammenfallende stykker rundt hullet
XlsxAssignRowHeightRun kopierer stykkene utenfor det tømte intervallet og legger ikke til noe for intervallet selv, så seriene holder seg usammenfallende ved konstruksjon og et oppslag kan stoppe ved første treff mens overstyringen på rad 500000 står urørt

Hvor blir stilene for arvede tomme celler av?

Inn i ett intervallstiloverlegg per kolonne, ikke i celleobjekter. OdsCommitRow avgjør per kolonneverdi om den er en kompakt tom celle: raden gjentas mer enn én gang, og cellen har ingen verdi, ingen formel og ingen rik tekst. For en kompakt tom celle oppretter den en ekte celle bare på den første raden i serien, bruker den arvede stilen på den, og registrerer så de samme seks stilindeksene — font, fyll, ramme, tallformat, justering, beskyttelse — som en StyleOverlays.Add som dekker rad to til slutten av serien i den kolonnen. Radene etter den første hoppes helt over i materialiseringsløkken

Regresjonstesten gjør formen konkret. Etter å ha åpnet et ark der den andre raden gjentas 1,048,575 ganger under en fet standardstil for kolonnen, hevdes det at Sheet.Cells.Count er under 10, og Sheet.Cells[700000, 1].FontIndex løser seg fortsatt til den fete fonten — overlegget leverer stilen i det øyeblikket den koordinaten berøres. Dette er den samme mekanismen som hindrer en formatert, men tom kolonne i å koste en million celler på XLSX-siden; notatene om cellelagring i radblokker og intervallstiloverlegg dekker hvordan overlegg legges i lag og løses opp. Det nye her er at ODS-importøren lager dem selv, ut fra gjentakelsestallet, i stedet for å vente på at en applikasjon formaterer et område

Hvordan skriver SaveAsODS gjentakelsestallet tilbake?

Ved å dele den tomme halen av arket bare der noe faktisk endrer seg. OdsBuildTableXml sporer nå to grenser: contentMaxRow, den siste raden som holder en verdi, formel, hyperkobling eller manuelt radskift, og maxRow, som i tillegg strekker seg gjennom tomme celler som bare har stil, enkelthøyder per rad, hver series LastRow og hver overleggs nederste kant. En tom celle med bare stil teller ikke lenger som innhold — TXLSXCells.IsStyleOnlyBlank er det som utelukker den — så den avsluttende stilerte raden i LibreOffice-filen slutter å dra innholdsgrensen ned til bunnen av arket

Over contentMaxRow skrives radene én om gangen, nøyaktig som før. Under den regner skriveren ut nextRow som den minste av: neste series FirstRow, gjeldende series LastRow + 1, neste enkelthøyde-oppføring, neste overleggskant og neste materialiserte celle. Alt fra gjeldende rad og opp til nextRow - 1 sendes så ut som én <table:table-row> med table:number-rows-repeated satt til differansen, med én <table:table-cell/> per kolonne og stilenavnet slik overlegget løser det opp, når et overlegg dekker den kolonnen. Radstilen selv kommer fra TOdsAutoStylePool.RowStyleFor(AHidden, ABreakBefore, AHeightSpec), som nå bretter høydeteksten — 12.81pt, for eksempel — inn i dedupliseringsnøkkelen sin sammen med flaggene for skjult og sideskift, slik at hver rad i serien deler én ro<N>-stil med én enkelt style:row-height-egenskap

Hva SaveAsODS skriver for et ark som bygger på en serie: contentMaxRow stopper på rad 45 der verdiene tar slutt, mens maxRow strekker seg gjennom høydeserien og overstyringene dens, radene over grensen skrives én om gangen, og halen sendes ut som gjentatte table-row-elementer der radstilen kommer fra RowStyleFor og cellestilene løses opp gjennom overleggene
Hvert gjentatte element spenner over én ensartet strekning og stopper ved neste seriekant, høydeoppføring, overleggskant eller materialiserte celle, så et ark uten valideringer lagres som en håndfull elementer, mens valideringer eller en XLSX-eksport betaler per rad
var
  Workbook, Reopened: TXLSXWorkbook;
  Saved: TMemoryStream;
begin
  Workbook := TXLSXWorkbook.Create;
  Reopened := TXLSXWorkbook.Create;
  Saved := TMemoryStream.Create;
  try
    Workbook.OpenODS('conditional-formatting.ods');
    Workbook.Sheets[1].RowHeight[500000] := 36;
    Workbook.Sheets[1].ClearRowHeight(500001);
    // Den tomme halen skrives som en håndfull gjentatte rader, ikke en million
    Workbook.SaveAsODS(Saved);
    Writeln('ODS size: ', Saved.Size, ' bytes');
    Saved.Position := 0;
    Reopened.Open(Saved);
    // Overstyringen, hullet og serien overlever alle tur-returen
    Writeln(Reopened.Sheets[1].RowHeight[500000]:0:2);   // 36.00
    Writeln(Reopened.Sheets[1].HasRowHeight(500001));    // False
    Writeln(Reopened.Sheets[1].RowHeight[500002]:0:2);   // seriehøyden
  finally
    Saved.Free;
    Reopened.Free;
    Workbook.Free;
  end;
end;

Testen som fester dette, hevder at den lagrede strømmen er under 64 KB for et ark der høydeserien spenner over 1,048,575 rader med en overstyring og et hull slått ut i midten. To ærlige grenser hører hjemme ved siden av det tallet. For det første setter et regnearkark med datavalideringer contentMaxRow til maxRow, så valideringer slår av halekomprimeringen på det arket, og det skrives rad for rad igjen. For det andre har XLSX ingen gjentakelsesattributt — en SpreadsheetML-<row> beskriver én rad — så eksport av et seriebasert ark til .xlsx ramser opp radene serien dekker og skriver et ht-attributt på hver av dem. Modellen forblir kompakt i minnet; filformatet bestemmer hvordan filen ser ut

Hva skylder hver radomnummererende operasjon seriene nå?

Vedlikehold. En ny representasjon av radmetadata er bare korrekt hvis hver operasjon som endrer radnummer, flytter den sammen med listene per rad som den ligger ved siden av, og commiten berører hver av disse operasjonene. InsertRows og DeleteRows går gjennom XlsxShiftRowHeightRuns, som bygger arrayet på nytt ved å beholde den delen av hver serie som ligger før redigeringspunktet, droppe alt som faller innenfor et slettevindu, og legge resten tilbake forskjøvet med deltaen — så en serie som står over en innsetting, blir to serier med et gap, og en serie som står over en sletting, krymper. TileRangeAxisMetadata tømmer seriene over hele det flislagte spennet og registrerer deretter hver kildeserie på nytt én gang per kopi ved sin forskyvning. TXLSXWorksheet.CopyFrom og TXLSXSheets.AddCopy tar en Copy() av arrayet i stedet for å tilordne det, og derfor kan testen tømme alle høyder på en klon og fortsatt finne originalarket intakt på rad 1,048,576

var
  Sheet: TXLSXWorksheet;
begin
  Sheet := Workbook.Sheets[1];
  Sheet.RowHeight[500000] := 36;
  Sheet.ClearRowHeight(500001);
  // Sett inn to rader på 500000: overstyringen flytter til 500002, hullet til 500003
  Sheet.InsertRows(500000, 2);
  Writeln(Sheet.RowHeight[500002]:0:2);   // 36.00
  Writeln(Sheet.HasRowHeight(500003));    // False
  // Slett dem igjen: alt forskyves tilbake
  Sheet.DeleteRows(500000, 2);
  Writeln(Sheet.RowHeight[500000]:0:2);   // 36.00
  // Flislegg rad 2..4 to ganger nedover arket; seriehøydene følger hver kopi
  Sheet.TileRangeAxisMetadata(2, 1, 3, 1, 2, 1);
  Writeln(Sheet.RowHeight[7]:0:2);        // seriehøyden
end;

Grensene på lesesiden har den samme forpliktelsen. GetUsedRange dytter nederkanten sin opp til hver series FirstRow og LastRow, og BuildRowMajorCellOrder utvider sin metadatainkluderende maksimumsrad gjennom hver serie, slik at XLSX-skriveren fortsatt besøker rader som bare har høyde. Hvis du noen gang legger til en egen radnøklet struktur oppå objektmodellen i HotXLS, er dette sjekklisten: sett inn, slett, flislegg, kopier, brukt område og enhver serialisering. Går du glipp av én, er feilen stille — høydene forskyves med antallet innsettinger, og ingenting kaster unntak

Hva forblir per rad, og hvordan tallene ser ut nå

Skjulte flagg, disposisjonsnivåer og sammenslått tilstand utvides fortsatt. Radavslutteren løper SetRowHidden og SetRowOutlineLevel én gang per gjentatte rad, så et ark som skjuler en hale på en million rader, eller legger den inn i en table:table-row-group, betaler en oppføring per rad for hvert av de attributtene. Endringen i 2.382.2 er avgrenset til de to tingene HXLS-005 faktisk målte — høyder og arvede tomme stiler — og den samme serieteknikken ville gjelde for de andre hvis en fil noen gang krevde det. ODS-leseren handler heller ikke på style:use-optimal-row-height; en radstil som sier «optimal» og oppgir en høyde, importeres med den høyden

Mot korpuset fullfører conditional-formatting.ods nå syklusen åpne, hevde, lagre, gjenåpne og hevde på nytt på 0.178 sekunder på Win32 og 0.158 sekunder på Win64, med selve åpningstrinnet på 0.020 sekunder, innenfor et budsjett på 60 sekunder som den tidligere sprengte. Grensesnittene på arbeidsboknivå som formatet flyter gjennom, er beskrevet i gjennomgangen av å åpne og lagre ODS-filer, og det bredere settet med grep for store filer i ytelse for store arbeidsbøker; selve ODF-rad-elementet, med sine attributter for gjentakelse og stil, er spesifisert i ODF 1.3 Part 3 §9.1.4

HotXLS leser og skriver XLS, XLSX og ODS fra innebygd Delphi- og C++Builder-kode uten at Excel eller LibreOffice er installert, og derfor er en gjentakelse på en million rader noe biblioteket må modellere godt i stedet for å sende videre til en ekstern prosess — siden for HotXLS Delphi-regnearkkomponenten lister opp formatene og RAD Studio-versjonene som støttes