Skip to content

Commit ad71eb3

Browse files
committed
LT-22122: Word Export performance improvement #2
There was data in a DocFragment that was only needed by the fragment that was going to write to the docx file. We now moved that data to a MasterDocFragment and only have a Body in the regular DocFragment. Regular DocFragments are frequently created and discarded. Not having them open a memory stream saves a significant amount of time. Combined with the performance improvements in commit e1943fb, export times are now 30% to 50% of previous times. Change-Id: I9ec6fa92709d3562c9331ec30c4c2920a71468b4
1 parent ddf8283 commit ad71eb3

File tree

2 files changed

+51
-53
lines changed

2 files changed

+51
-53
lines changed

Src/xWorks/LcmWordGenerator.cs

Lines changed: 32 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ public class LcmWordGenerator : ILcmContentGenerator, ILcmStylesGenerator
4545

4646
private ReadOnlyPropertyTable _propertyTable;
4747
public static bool IsBidi { get; private set; }
48-
private static DocFragment MasterFragment { get; set; }
48+
private static MasterDocFragment MasterFragment { get; set; }
4949

5050
public LcmWordGenerator(LcmCache cache)
5151
{
@@ -63,7 +63,7 @@ public static void SavePublishedDocx(int[] entryHvos, DictionaryPublicationDecor
6363
{
6464
using (MemoryStream mem = new MemoryStream())
6565
{
66-
MasterFragment = new DocFragment(mem);
66+
MasterFragment = new MasterDocFragment(mem);
6767

6868
var entryCount = entryHvos.Length;
6969
var cssPath = System.IO.Path.ChangeExtension(filePath, "css");
@@ -346,50 +346,55 @@ internal static IFragment GenerateLetterHeaderIfNeeded(ICmObject entry, ref stri
346346
return DocFragment.GenerateLetterHeaderDocFragment(headerTextBuilder.ToString(), WordStylesGenerator.LetterHeadingDisplayName, firstHeader, wsString);
347347
}
348348

349-
/*
350-
* DocFragment Region
351-
*/
352-
#region DocFragment class
353-
public class DocFragment : IFragment
349+
#region MasterDocFragment class
350+
/// <summary>
351+
/// The MasterDocFragment contains the data to write to a docx file. Regular
352+
/// DocFragments that are used to build the MasterDocFragment do not need
353+
/// this data.
354+
/// </summary>
355+
public class MasterDocFragment : DocFragment
354356
{
355357
internal MemoryStream MemStr { get; }
356358
internal WordprocessingDocument DocFrag { get; }
357359
internal MainDocumentPart mainDocPart { get; }
358-
internal WP.Body DocBody { get; }
359360

360361
/// <summary>
361-
/// Constructs a new memory stream and creates an empty doc fragment
362-
/// that writes to that stream.
362+
/// Initializes the memory stream from the argument and creates
363+
/// an empty master doc fragment that writes to that stream.
363364
/// </summary>
364-
public DocFragment()
365+
public MasterDocFragment(MemoryStream str)
365366
{
366-
MemStr = new MemoryStream();
367-
DocFrag = WordprocessingDocument.Open(MemStr, true);
367+
MemStr = str;
368+
DocFrag = WordprocessingDocument.Open(str, true);
368369

369370
// Initialize the document and body.
370371
mainDocPart = DocFrag.AddMainDocumentPart();
371372
mainDocPart.Document = new WP.Document();
372-
DocBody = mainDocPart.Document.AppendChild(new WP.Body());
373+
mainDocPart.Document.AppendChild(DocBody);
373374
}
374375

376+
}
377+
#endregion MasterDocFragment class
378+
379+
/*
380+
* DocFragment Region
381+
*/
382+
#region DocFragment class
383+
public class DocFragment : IFragment
384+
{
385+
internal WP.Body DocBody { get; }
386+
375387
/// <summary>
376-
/// Initializes the memory stream from the argument and creates
377-
/// an empty doc fragment that writes to that stream.
388+
/// Constructs an empty doc fragment that has a Body.
378389
/// </summary>
379-
public DocFragment(MemoryStream str)
390+
public DocFragment()
380391
{
381-
MemStr = str;
382-
DocFrag = WordprocessingDocument.Open(str, true);
383-
384-
// Initialize the document and body.
385-
mainDocPart = DocFrag.AddMainDocumentPart();
386-
mainDocPart.Document = new WP.Document();
387-
DocBody = mainDocPart.Document.AppendChild(new WP.Body());
392+
DocBody = new WP.Body();
388393
}
389394

390395
/// <summary>
391-
/// Constructs a new memory stream and creates a non-empty doc fragment,
392-
/// containing the given string, that writes to that stream.
396+
/// Constructs a non-empty doc fragment that has a Body which
397+
/// contains the given string.
393398
/// </summary>
394399
public DocFragment(string str) : this()
395400
{
@@ -688,7 +693,7 @@ public void AppendSpace()
688693
public bool IsNullOrEmpty()
689694
{
690695
// A docbody with no children is an empty document.
691-
if (MemStr == null || DocFrag == null || DocBody == null || !DocBody.HasChildren)
696+
if (DocBody == null || !DocBody.HasChildren)
692697
{
693698
return true;
694699
}
@@ -941,7 +946,6 @@ public void Dispose()
941946

942947
public void Flush()
943948
{
944-
WordFragment.MemStr.Flush();
945949
}
946950

947951
public void Insert(IFragment frag)
@@ -1775,12 +1779,6 @@ public IFragment AddImage(ConfigurableDictionaryNode config, ConfiguredLcmGenera
17751779
// calculate the maximum image height from the configuration
17761780
var maxHeight = config.Model.Pictures?.Height ?? (picOpts?.MaximumHeight ?? 1.0f);
17771781
Drawing image = CreateImage(srcAttribute, partId, maxWidth, maxHeight);
1778-
1779-
if (imageFrag.DocFrag.MainDocumentPart is null || imageFrag.DocFrag.MainDocumentPart.Document.Body is null)
1780-
{
1781-
throw new ArgumentNullException("MainDocumentPart and/or Body is null.");
1782-
}
1783-
17841782
Run imgRun = new Run();
17851783
imgRun.AppendChild(image);
17861784

Src/xWorks/xWorksTests/LcmWordGeneratorTests.cs

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -240,8 +240,8 @@ public void GenerateWordDocForEntry_OneSenseWithGlossGeneratesCorrectResult()
240240
//SUT
241241
var result = ConfiguredLcmGenerator.GenerateContentForEntry(entry, mainEntryNode, null, DefaultSettings, 0) as DocFragment;
242242
Console.WriteLine(result);
243-
AssertThatXmlIn.String(result.mainDocPart.RootElement.OuterXml).HasSpecifiedNumberOfMatchesForXpath(
244-
"/w:document/w:body/w:p/w:r/w:t[text()='gloss']",
243+
AssertThatXmlIn.String(result.DocBody.OuterXml).HasSpecifiedNumberOfMatchesForXpath(
244+
"/w:body/w:p/w:r/w:t[text()='gloss']",
245245
1,
246246
WordNamespaceManager);
247247
}
@@ -275,8 +275,8 @@ public void GenerateWordDocForEntry_LineBreaksInBeforeContentWork()
275275
//SUT
276276
var result = ConfiguredLcmGenerator.GenerateContentForEntry(entry, mainEntryNode, null, DefaultSettings, 0) as DocFragment;
277277
Console.WriteLine(result);
278-
AssertThatXmlIn.String(result?.mainDocPart.RootElement?.OuterXml).HasSpecifiedNumberOfMatchesForXpath(
279-
"/w:document/w:body/w:p/w:r/w:br[@w:type='textWrapping']",
278+
AssertThatXmlIn.String(result?.DocBody?.OuterXml).HasSpecifiedNumberOfMatchesForXpath(
279+
"/w:body/w:p/w:r/w:br[@w:type='textWrapping']",
280280
2,
281281
WordNamespaceManager);
282282
}
@@ -327,8 +327,8 @@ public void GenerateUniqueStyleName()
327327
//SUT
328328
var result = ConfiguredLcmGenerator.GenerateContentForEntry(entry, mainEntryNode, null, DefaultSettings, 0) as DocFragment;
329329

330-
Assert.True(result.mainDocPart.RootElement.OuterXml.Contains("Gloss[lang=en]"));
331-
Assert.True(result.mainDocPart.RootElement.OuterXml.Contains("Gloss2[lang=en]"));
330+
Assert.True(result.DocBody.OuterXml.Contains("Gloss[lang=en]"));
331+
Assert.True(result.DocBody.OuterXml.Contains("Gloss2[lang=en]"));
332332
}
333333

334334
[Test]
@@ -385,7 +385,7 @@ public void GenerateSenseNumberData()
385385
// 3. Sense number: 2
386386
// 4. Sense number after text: AFT
387387
const string senseNumberTwoRun = "<w:t xml:space=\"preserve\">BEF</w:t></w:r><w:r><w:rPr><w:rStyle w:val=\"Sense Number[lang=en]\" /></w:rPr><w:t xml:space=\"preserve\">2</w:t></w:r><w:r><w:rPr><w:rStyle w:val=\"Sense Number-Context[lang=en]\" /></w:rPr><w:t xml:space=\"preserve\">AFT</w:t></w:r>";
388-
Assert.True(result.mainDocPart.RootElement.OuterXml.Contains(senseNumberTwoRun));
388+
Assert.True(result.DocBody.OuterXml.Contains(senseNumberTwoRun));
389389
}
390390

391391
[Test]
@@ -438,7 +438,7 @@ public void GenerateBeforeBetweenAfterContent()
438438

439439
//SUT
440440
var result = ConfiguredLcmGenerator.GenerateContentForEntry(testEntry, mainEntryNode, null, DefaultSettings, 0) as DocFragment;
441-
var outXml = result.mainDocPart.RootElement.OuterXml;
441+
var outXml = result.DocBody.OuterXml;
442442

443443
// Before text 'BE1' is before sense number '1' for 'gloss'.
444444
const string beforeFirstSense =
@@ -520,7 +520,7 @@ public void GenerateBeforeBetweenAfterContentWithWSAbbreviation()
520520

521521
//SUT
522522
var result = ConfiguredLcmGenerator.GenerateContentForEntry(testEntry, mainEntryNode, null, DefaultSettings, 0) as DocFragment;
523-
var outXml = result.mainDocPart.RootElement.OuterXml;
523+
var outXml = result.DocBody.OuterXml;
524524

525525
// Before text 'BE3' is after the sense number '1' and before the english abbreviation, which is before 'gloss'.
526526
const string beforeAbbreviation =
@@ -576,7 +576,7 @@ public void GeneratePropertyData()
576576

577577
//SUT
578578
var result = ConfiguredLcmGenerator.GenerateContentForEntry(entry, mainEntryNode, null, DefaultSettings, 0) as DocFragment;
579-
var outXml = result.mainDocPart.RootElement.OuterXml;
579+
var outXml = result.DocBody.OuterXml;
580580

581581
// The property before text 'BE4' is first, followed by the style that is applied to the property, 'DisplayNameBase'.
582582
const string beforeAndStyle = "<w:t xml:space=\"preserve\">BE4</w:t></w:r><w:r><w:rPr><w:rStyle w:val=\"DisplayNameBase[lang=en]\" /></w:rPr>";
@@ -641,7 +641,7 @@ public void EmbeddedStylesHaveNoExtraSpace()
641641

642642
//SUT
643643
var result = ConfiguredLcmGenerator.GenerateContentForEntry(testEntry, mainEntryNode, null, DefaultSettings, 0) as DocFragment;
644-
var outXml = result.mainDocPart.RootElement.OuterXml;
644+
var outXml = result.DocBody.OuterXml;
645645
// Verify that AREYOUCRAZY appears only once in the output.
646646
var betweenCount = Regex.Matches(outXml, "AREYOUCRAZY").Count;
647647

@@ -688,8 +688,8 @@ public void ReferenceParagraphDisplayNames()
688688
var result = ConfiguredLcmGenerator.GenerateContentForEntry(testEntry, mainEntryNode, null, DefaultSettings, 0) as DocFragment;
689689

690690
// Assert that the references to the paragraph styles use the display names, not the style names.
691-
Assert.True(result.mainDocPart.RootElement.OuterXml.Contains(MainEntryParagraphDisplayName));
692-
Assert.True(result.mainDocPart.RootElement.OuterXml.Contains(SensesParagraphDisplayName));
691+
Assert.True(result.DocBody.OuterXml.Contains(MainEntryParagraphDisplayName));
692+
Assert.True(result.DocBody.OuterXml.Contains(SensesParagraphDisplayName));
693693
}
694694

695695
[Test]
@@ -751,8 +751,8 @@ public void GenerateParagraphForSensesAndSubSenses()
751751
var result = ConfiguredLcmGenerator.GenerateContentForEntry(testEntry, mainEntryNode, null, DefaultSettings, 0) as DocFragment;
752752

753753
// There should be 5 paragraphs, one for the main entry, one for each sense, and one for each subsense.
754-
AssertThatXmlIn.String(result.mainDocPart.RootElement.OuterXml).HasSpecifiedNumberOfMatchesForXpath(
755-
"/w:document/w:body/w:p",
754+
AssertThatXmlIn.String(result.DocBody.OuterXml).HasSpecifiedNumberOfMatchesForXpath(
755+
"/w:body/w:p",
756756
5,
757757
WordNamespaceManager);
758758
}
@@ -815,7 +815,7 @@ public void GenerateBulletsAndNumbering()
815815
var result = ConfiguredLcmGenerator.GenerateContentForEntry(testEntry, mainEntryNode, null, DefaultSettings, 0) as DocFragment;
816816

817817
// There should be two instances of the bulletId and one instance for each of the numberId's.
818-
string resultStr = result.mainDocPart.RootElement.OuterXml;
818+
string resultStr = result.DocBody.OuterXml;
819819
int count1 = Regex.Matches(resultStr, "<w:numId w:val=\"1\" />").Count;
820820
int count2 = Regex.Matches(resultStr, "<w:numId w:val=\"2\" />").Count;
821821
int count3 = Regex.Matches(resultStr, "<w:numId w:val=\"3\" />").Count;
@@ -888,13 +888,13 @@ public void GenerateContinueParagraph()
888888
var result = ConfiguredLcmGenerator.GenerateContentForEntry(testEntry, mainEntryNode, null, DefaultSettings, 0) as DocFragment;
889889

890890
// There should be 3 paragraph styles, one for the main entry, one for the sense, and one for the continuation of the main entry.
891-
AssertThatXmlIn.String(result.mainDocPart.RootElement.OuterXml).HasSpecifiedNumberOfMatchesForXpath(
892-
"/w:document/w:body/w:p/w:pPr/w:pStyle",
891+
AssertThatXmlIn.String(result.DocBody.OuterXml).HasSpecifiedNumberOfMatchesForXpath(
892+
"/w:body/w:p/w:pPr/w:pStyle",
893893
3,
894894
WordNamespaceManager);
895895

896896
// Assert that the continuation paragraph uses the continuation style.
897-
Assert.True(result.mainDocPart.RootElement.OuterXml.Contains(MainEntryParagraphDisplayName + WordStylesGenerator.EntryStyleContinue));
897+
Assert.True(result.DocBody.OuterXml.Contains(MainEntryParagraphDisplayName + WordStylesGenerator.EntryStyleContinue));
898898
}
899899

900900
[Test]

0 commit comments

Comments
 (0)