Skip to content

Commit 2f08811

Browse files
committedAug 7, 2018
[DebugInfo] Reduce debug_str_offsets section size
Summary: The accelerator tables use the debug_str section to store their strings. However, they do not support the indirect method of access that is available for the debug_info section (DW_FORM_strx et al.). Currently our code is assuming that all strings can/will be referenced indirectly, and puts all of them into the debug_str_offsets section. This is generally true for regular (unsplit) dwarf, but in the DWO case, most of the strings in the debug_str section will only be used from the accelerator tables. Therefore the contents of the debug_str_offsets section will be largely unused and bloating the main executable. This patch rectifies this by teaching the DwarfStringPool to differentiate between strings accessed directly and indirectly. When a user inserts a string into the pool it has to declare whether that string will be referenced directly or not. If at least one user requsts indirect access, that string will be assigned an index ID and put into debug_str_offsets table. Otherwise, the offset table is skipped. This approach reduces the overall binary size (when compiled with -gdwarf-5 -gsplit-dwarf) in my tests by about 2% (debug_str_offsets is shrunk by 99%). Reviewers: probinson, dblaikie, JDevlieghere Subscribers: aprantl, mgrang, llvm-commits Differential Revision: https://reviews.llvm.org/D49493 llvm-svn: 339122
1 parent 7e18938 commit 2f08811

14 files changed

+298
-56
lines changed
 

‎llvm/include/llvm/CodeGen/DwarfStringPoolEntry.h

+33-14
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
#ifndef LLVM_CODEGEN_DWARFSTRINGPOOLENTRY_H
1111
#define LLVM_CODEGEN_DWARFSTRINGPOOLENTRY_H
1212

13+
#include "llvm/ADT/PointerIntPair.h"
1314
#include "llvm/ADT/StringMap.h"
1415

1516
namespace llvm {
@@ -18,34 +19,52 @@ class MCSymbol;
1819

1920
/// Data for a string pool entry.
2021
struct DwarfStringPoolEntry {
22+
static constexpr unsigned NotIndexed = -1;
23+
2124
MCSymbol *Symbol;
2225
unsigned Offset;
2326
unsigned Index;
27+
28+
bool isIndexed() const { return Index != NotIndexed; }
2429
};
2530

2631
/// String pool entry reference.
27-
struct DwarfStringPoolEntryRef {
28-
const StringMapEntry<DwarfStringPoolEntry> *I = nullptr;
32+
class DwarfStringPoolEntryRef {
33+
PointerIntPair<const StringMapEntry<DwarfStringPoolEntry> *, 1, bool>
34+
MapEntryAndIndexed;
35+
36+
const StringMapEntry<DwarfStringPoolEntry> *getMapEntry() const {
37+
return MapEntryAndIndexed.getPointer();
38+
}
2939

3040
public:
3141
DwarfStringPoolEntryRef() = default;
32-
explicit DwarfStringPoolEntryRef(
33-
const StringMapEntry<DwarfStringPoolEntry> &I)
34-
: I(&I) {}
42+
DwarfStringPoolEntryRef(const StringMapEntry<DwarfStringPoolEntry> &Entry,
43+
bool Indexed)
44+
: MapEntryAndIndexed(&Entry, Indexed) {}
3545

36-
explicit operator bool() const { return I; }
46+
explicit operator bool() const { return getMapEntry(); }
3747
MCSymbol *getSymbol() const {
38-
assert(I->second.Symbol && "No symbol available!");
39-
return I->second.Symbol;
48+
assert(getMapEntry()->second.Symbol && "No symbol available!");
49+
return getMapEntry()->second.Symbol;
4050
}
41-
unsigned getOffset() const { return I->second.Offset; }
42-
unsigned getIndex() const { return I->second.Index; }
43-
StringRef getString() const { return I->first(); }
51+
unsigned getOffset() const { return getMapEntry()->second.Offset; }
52+
bool isIndexed() const { return MapEntryAndIndexed.getInt(); }
53+
unsigned getIndex() const {
54+
assert(isIndexed());
55+
assert(getMapEntry()->getValue().isIndexed());
56+
return getMapEntry()->second.Index;
57+
}
58+
StringRef getString() const { return getMapEntry()->first(); }
4459
/// Return the entire string pool entry for convenience.
45-
DwarfStringPoolEntry getEntry() const { return I->getValue(); }
60+
DwarfStringPoolEntry getEntry() const { return getMapEntry()->getValue(); }
4661

47-
bool operator==(const DwarfStringPoolEntryRef &X) const { return I == X.I; }
48-
bool operator!=(const DwarfStringPoolEntryRef &X) const { return I != X.I; }
62+
bool operator==(const DwarfStringPoolEntryRef &X) const {
63+
return getMapEntry() == X.getMapEntry();
64+
}
65+
bool operator!=(const DwarfStringPoolEntryRef &X) const {
66+
return getMapEntry() != X.getMapEntry();
67+
}
4968
};
5069

5170
} // end namespace llvm

‎llvm/lib/CodeGen/AsmPrinter/DwarfDebug.cpp

+1-2
Original file line numberDiff line numberDiff line change
@@ -2437,8 +2437,7 @@ void DwarfDebug::addAccelNameImpl(AccelTable<DataT> &AppleAccel, StringRef Name,
24372437
return;
24382438

24392439
DwarfFile &Holder = useSplitDwarf() ? SkeletonHolder : InfoHolder;
2440-
DwarfStringPoolEntryRef Ref =
2441-
Holder.getStringPool().getEntry(*Asm, Name);
2440+
DwarfStringPoolEntryRef Ref = Holder.getStringPool().getEntry(*Asm, Name);
24422441

24432442
switch (getAccelTableKind()) {
24442443
case AccelTableKind::Apple:

‎llvm/lib/CodeGen/AsmPrinter/DwarfStringPool.cpp

+39-11
Original file line numberDiff line numberDiff line change
@@ -24,25 +24,39 @@ DwarfStringPool::DwarfStringPool(BumpPtrAllocator &A, AsmPrinter &Asm,
2424
: Pool(A), Prefix(Prefix),
2525
ShouldCreateSymbols(Asm.MAI->doesDwarfUseRelocationsAcrossSections()) {}
2626

27-
DwarfStringPool::EntryRef DwarfStringPool::getEntry(AsmPrinter &Asm,
28-
StringRef Str) {
27+
StringMapEntry<DwarfStringPool::EntryTy> &
28+
DwarfStringPool::getEntryImpl(AsmPrinter &Asm, StringRef Str) {
2929
auto I = Pool.insert(std::make_pair(Str, EntryTy()));
30+
auto &Entry = I.first->second;
3031
if (I.second) {
31-
auto &Entry = I.first->second;
32-
Entry.Index = Pool.size() - 1;
32+
Entry.Index = EntryTy::NotIndexed;
3333
Entry.Offset = NumBytes;
3434
Entry.Symbol = ShouldCreateSymbols ? Asm.createTempSymbol(Prefix) : nullptr;
3535

3636
NumBytes += Str.size() + 1;
3737
assert(NumBytes > Entry.Offset && "Unexpected overflow");
3838
}
39-
return EntryRef(*I.first);
39+
return *I.first;
40+
}
41+
42+
DwarfStringPool::EntryRef DwarfStringPool::getEntry(AsmPrinter &Asm,
43+
StringRef Str) {
44+
auto &MapEntry = getEntryImpl(Asm, Str);
45+
return EntryRef(MapEntry, false);
46+
}
47+
48+
DwarfStringPool::EntryRef DwarfStringPool::getIndexedEntry(AsmPrinter &Asm,
49+
StringRef Str) {
50+
auto &MapEntry = getEntryImpl(Asm, Str);
51+
if (!MapEntry.getValue().isIndexed())
52+
MapEntry.getValue().Index = NumIndexedStrings++;
53+
return EntryRef(MapEntry, true);
4054
}
4155

4256
void DwarfStringPool::emitStringOffsetsTableHeader(AsmPrinter &Asm,
4357
MCSection *Section,
4458
MCSymbol *StartSym) {
45-
if (empty())
59+
if (getNumIndexedStrings() == 0)
4660
return;
4761
Asm.OutStreamer->SwitchSection(Section);
4862
unsigned EntrySize = 4;
@@ -51,7 +65,7 @@ void DwarfStringPool::emitStringOffsetsTableHeader(AsmPrinter &Asm,
5165
// table. The header consists of an entry with the contribution's
5266
// size (not including the size of the length field), the DWARF version and
5367
// 2 bytes of padding.
54-
Asm.emitInt32(size() * EntrySize + 4);
68+
Asm.emitInt32(getNumIndexedStrings() * EntrySize + 4);
5569
Asm.emitInt16(Asm.getDwarfVersion());
5670
Asm.emitInt16(0);
5771
// Define the symbol that marks the start of the contribution. It is
@@ -69,12 +83,18 @@ void DwarfStringPool::emit(AsmPrinter &Asm, MCSection *StrSection,
6983
// Start the dwarf str section.
7084
Asm.OutStreamer->SwitchSection(StrSection);
7185

72-
// Get all of the string pool entries and put them in an array by their ID so
73-
// we can sort them.
74-
SmallVector<const StringMapEntry<EntryTy> *, 64> Entries(Pool.size());
86+
// Get all of the string pool entries and sort them by their offset.
87+
SmallVector<const StringMapEntry<EntryTy> *, 64> Entries;
88+
Entries.reserve(Pool.size());
7589

7690
for (const auto &E : Pool)
77-
Entries[E.getValue().Index] = &E;
91+
Entries.push_back(&E);
92+
93+
llvm::sort(
94+
Entries.begin(), Entries.end(),
95+
[](const StringMapEntry<EntryTy> *A, const StringMapEntry<EntryTy> *B) {
96+
return A->getValue().Offset < B->getValue().Offset;
97+
});
7898

7999
for (const auto &Entry : Entries) {
80100
assert(ShouldCreateSymbols == static_cast<bool>(Entry->getValue().Symbol) &&
@@ -93,6 +113,14 @@ void DwarfStringPool::emit(AsmPrinter &Asm, MCSection *StrSection,
93113

94114
// If we've got an offset section go ahead and emit that now as well.
95115
if (OffsetSection) {
116+
// Now only take the indexed entries and put them in an array by their ID so
117+
// we can emit them in order.
118+
Entries.resize(NumIndexedStrings);
119+
for (const auto &Entry : Pool) {
120+
if (Entry.getValue().isIndexed())
121+
Entries[Entry.getValue().Index] = &Entry;
122+
}
123+
96124
Asm.OutStreamer->SwitchSection(OffsetSection);
97125
unsigned size = 4; // FIXME: DWARF64 is 8.
98126
for (const auto &Entry : Entries)

‎llvm/lib/CodeGen/AsmPrinter/DwarfStringPool.h

+10
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,11 @@ class DwarfStringPool {
3030
StringMap<EntryTy, BumpPtrAllocator &> Pool;
3131
StringRef Prefix;
3232
unsigned NumBytes = 0;
33+
unsigned NumIndexedStrings = 0;
3334
bool ShouldCreateSymbols;
3435

36+
StringMapEntry<EntryTy> &getEntryImpl(AsmPrinter &Asm, StringRef Str);
37+
3538
public:
3639
using EntryRef = DwarfStringPoolEntryRef;
3740

@@ -48,8 +51,15 @@ class DwarfStringPool {
4851

4952
unsigned size() const { return Pool.size(); }
5053

54+
unsigned getNumIndexedStrings() const { return NumIndexedStrings; }
55+
5156
/// Get a reference to an entry in the string pool.
5257
EntryRef getEntry(AsmPrinter &Asm, StringRef Str);
58+
59+
/// Same as getEntry, except that you can use EntryRef::getIndex to obtain a
60+
/// unique ID of this entry (e.g., for use in indexed forms like
61+
/// DW_FORM_strx).
62+
EntryRef getIndexedEntry(AsmPrinter &Asm, StringRef Str);
5363
};
5464

5565
} // end namespace llvm

‎llvm/lib/CodeGen/AsmPrinter/DwarfUnit.cpp

+6-1
Original file line numberDiff line numberDiff line change
@@ -243,9 +243,14 @@ void DwarfUnit::addString(DIE &Die, dwarf::Attribute Attribute,
243243
DIEInlineString(String, DIEValueAllocator));
244244
return;
245245
}
246-
auto StringPoolEntry = DU->getStringPool().getEntry(*Asm, String);
247246
dwarf::Form IxForm =
248247
isDwoUnit() ? dwarf::DW_FORM_GNU_str_index : dwarf::DW_FORM_strp;
248+
249+
auto StringPoolEntry =
250+
useSegmentedStringOffsetsTable() || IxForm == dwarf::DW_FORM_GNU_str_index
251+
? DU->getStringPool().getIndexedEntry(*Asm, String)
252+
: DU->getStringPool().getEntry(*Asm, String);
253+
249254
// For DWARF v5 and beyond, use the smallest strx? form possible.
250255
if (useSegmentedStringOffsetsTable()) {
251256
IxForm = dwarf::DW_FORM_strx1;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
; REQUIRES: object-emission
2+
; RUN: llc -mtriple=x86_64-unknown-linux-gnu -split-dwarf-file=foo.dwo -filetype=obj < %s \
3+
; RUN: | llvm-dwarfdump -v - | FileCheck %s
4+
5+
; This triggers a situation where the order of entries in the .debug_str and
6+
; .debug_str_offsets sections does not match and makes sure that all entries are
7+
; still wired up correctly.
8+
9+
; Produced with "clang -S -emit-llvm -gdwarf-5" from source "int X;", copied
10+
; three times and modified by hand.
11+
12+
; CHECK: .debug_info contents:
13+
; CHECK: DW_TAG_compile_unit
14+
; CHECK: DW_AT_comp_dir [DW_FORM_strx1] ( indexed (00000001) string = "X3")
15+
; CHECK: DW_TAG_compile_unit
16+
; CHECK: DW_AT_comp_dir [DW_FORM_strx1] ( indexed (00000002) string = "X2")
17+
; CHECK: DW_TAG_compile_unit
18+
; CHECK: DW_AT_comp_dir [DW_FORM_strx1] ( indexed (00000003) string = "X1")
19+
; CHECK: .debug_info.dwo contents:
20+
21+
; CHECK: .debug_str contents:
22+
; CHECK: 0x[[X3:[0-9a-f]*]]: "X3"
23+
; CHECK: 0x[[X1:[0-9a-f]*]]: "X1"
24+
; CHECK: 0x[[X2:[0-9a-f]*]]: "X2"
25+
26+
; CHECK: .debug_str_offsets contents:
27+
; CHECK: Format = DWARF32, Version = 5
28+
; CHECK-NEXT: 00000000 "foo.dwo"
29+
; CHECK-NEXT: [[X3]] "X3"
30+
; CHECK-NEXT: [[X2]] "X2"
31+
; CHECK-NEXT: [[X1]] "X1"
32+
; CHECK-EMPTY:
33+
34+
35+
36+
!llvm.dbg.cu = !{!10, !20, !30}
37+
!llvm.module.flags = !{!0, !1, !2}
38+
!llvm.ident = !{!3}
39+
40+
!0 = !{i32 2, !"Dwarf Version", i32 5}
41+
!1 = !{i32 2, !"Debug Info Version", i32 3}
42+
!2 = !{i32 1, !"wchar_size", i32 4}
43+
!3 = !{!"clang version 7.0.0 (trunk 337353) (llvm/trunk 337361)"}
44+
45+
46+
@X1 = dso_local global i32 0, align 4, !dbg !11
47+
48+
!10 = distinct !DICompileUnit(language: DW_LANG_C_plus_plus, file: !13, producer: "clang version 7.0.0 (trunk 337353) (llvm/trunk 337361)", isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug, enums: !14, globals: !15)
49+
!11 = !DIGlobalVariableExpression(var: !12, expr: !DIExpression())
50+
!12 = distinct !DIGlobalVariable(name: "X1", scope: !10, file: !16, line: 1, type: !17, isLocal: false, isDefinition: true)
51+
!13 = !DIFile(filename: "-", directory: "X3", checksumkind: CSK_MD5, checksum: "f2e6e10e303927a308f1645fbf6f710e")
52+
!14 = !{}
53+
!15 = !{!11}
54+
!16 = !DIFile(filename: "<stdin>", directory: "X3", checksumkind: CSK_MD5, checksum: "f2e6e10e303927a308f1645fbf6f710e")
55+
!17 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
56+
57+
58+
@X2 = dso_local global i32 0, align 4, !dbg !21
59+
60+
!20 = distinct !DICompileUnit(language: DW_LANG_C_plus_plus, file: !23, producer: "clang version 7.0.0 (trunk 337353) (llvm/trunk 337361)", isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug, enums: !24, globals: !25)
61+
!21 = !DIGlobalVariableExpression(var: !22, expr: !DIExpression())
62+
!22 = distinct !DIGlobalVariable(name: "X2", scope: !20, file: !26, line: 1, type: !27, isLocal: false, isDefinition: true)
63+
!23 = !DIFile(filename: "-", directory: "X2", checksumkind: CSK_MD5, checksum: "f2e6e10e303927a308f1645fbf6f710e")
64+
!24 = !{}
65+
!25 = !{!21}
66+
!26 = !DIFile(filename: "<stdin>", directory: "X2", checksumkind: CSK_MD5, checksum: "f2e6e10e303927a308f1645fbf6f710e")
67+
!27 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
68+
69+
70+
@X3 = dso_local global i32 0, align 4, !dbg !31
71+
72+
!30 = distinct !DICompileUnit(language: DW_LANG_C_plus_plus, file: !33, producer: "clang version 7.0.0 (trunk 337353) (llvm/trunk 337361)", isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug, enums: !34, globals: !35)
73+
!31 = !DIGlobalVariableExpression(var: !32, expr: !DIExpression())
74+
!32 = distinct !DIGlobalVariable(name: "X3", scope: !30, file: !36, line: 1, type: !37, isLocal: false, isDefinition: true)
75+
!33 = !DIFile(filename: "-", directory: "X1", checksumkind: CSK_MD5, checksum: "f2e6e10e303927a308f1645fbf6f710e")
76+
!34 = !{}
77+
!35 = !{!31}
78+
!36 = !DIFile(filename: "<stdin>", directory: "X1", checksumkind: CSK_MD5, checksum: "f2e6e10e303927a308f1645fbf6f710e")
79+
!37 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)

‎llvm/test/DebugInfo/X86/string-offsets-table.ll

+15-11
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
; REQUIRES: object-emission
22
; RUN: llc -mtriple=x86_64-unknown-linux-gnu -filetype=obj < %s | llvm-dwarfdump -v - \
33
; RUN: | FileCheck --check-prefix=MONOLITHIC %s
4-
; RUN: llc -mtriple=x86_64-unknown-linux-gnu -split-dwarf-file=%t.dwo -filetype=obj < %s \
4+
; RUN: llc -mtriple=x86_64-unknown-linux-gnu -split-dwarf-file=foo.dwo -filetype=obj < %s \
55
; RUN: | llvm-dwarfdump -v - | FileCheck --check-prefix=SPLIT %s
66

77
; This basic test checks the emission of a DWARF v5 string offsets table in
@@ -59,6 +59,8 @@
5959
; SPLIT: DW_TAG_compile_unit
6060
; SPLIT-NOT: {{DW_TAG|contents:}}
6161
; SPLIT: DW_AT_str_offsets_base [DW_FORM_sec_offset] (0x00000008)
62+
; SPLIT: DW_AT_GNU_dwo_name [DW_FORM_strx1] ( indexed (00000000) string = "foo.dwo")
63+
; SPLIT: DW_AT_comp_dir [DW_FORM_strx1] ( indexed (00000001) string = "/home/test")
6264

6365
; Check for the split CU in .debug_info.dwo.
6466
; SPLIT: .debug_info.dwo contents:
@@ -79,10 +81,10 @@
7981
;
8082
; Extract the string offsets referenced in the main file by the skeleton unit.
8183
; SPLIT: .debug_str contents:
82-
; SPLIT-NEXT: 0x00000000:{{.*}}
83-
; SPLIT-NEXT: 0x[[STRING2SPLIT:[0-9a-f]*]]{{.*}}
84-
; SPLIT-NEXT: 0x[[STRING3SPLIT:[0-9a-f]*]]{{.*}}
85-
; SPLIT-NEXT: 0x[[STRING4SPLIT:[0-9a-f]*]]{{.*}}
84+
; SPLIT-NEXT: 0x00000000: "foo.dwo"
85+
; SPLIT-NEXT: 0x[[STRING2SPLIT:[0-9a-f]*]]: "/home/test"
86+
; SPLIT-NEXT: 0x[[STRING3SPLIT:[0-9a-f]*]]: "E"
87+
; SPLIT-NEXT: 0x[[STRING4SPLIT:[0-9a-f]*]]: "glob"
8688
;
8789
; Extract the string offsets referenced in the .dwo file by the split unit.
8890
; SPLIT: .debug_str.dwo contents:
@@ -91,13 +93,15 @@
9193
; SPLIT-NEXT: 0x[[STRING3DWO:[0-9a-f]*]]{{.*}}
9294
;
9395
; Check the string offsets sections in both the main and the .dwo files and
94-
; verify that the extracted string offsets are referenced correctly.
96+
; verify that the extracted string offsets are referenced correctly. The
97+
; sections should contain only the offsets of strings that are actually
98+
; referenced by the debug info.
9599
; SPLIT: .debug_str_offsets contents:
96-
; SPLIT-NEXT: 0x00000000: Contribution size = 20, Format = DWARF32, Version = 5
97-
; SPLIT-NEXT: 0x00000008: 00000000{{.*}}
98-
; SPLIT-NEXT: 0x0000000c: [[STRING2SPLIT]]
99-
; SPLIT-NEXT: 0x00000010: [[STRING3SPLIT]]
100-
; SPLIT-NEXT: 0x00000014: [[STRING4SPLIT]]
100+
; SPLIT-NEXT: 0x00000000: Contribution size = 12, Format = DWARF32, Version = 5
101+
; SPLIT-NEXT: 0x00000008: 00000000 "foo.dwo"
102+
; SPLIT-NEXT: 0x0000000c: [[STRING2SPLIT]] "/home/test"
103+
; SPLIT-EMPTY:
104+
101105
; SPLIT: .debug_str_offsets.dwo contents:
102106
; SPLIT-NEXT: 0x00000000: Contribution size = 36, Format = DWARF32, Version = 5
103107
; SPLIT-NEXT: 0x00000008: 00000000{{.*}}

‎llvm/tools/dsymutil/DwarfStreamer.cpp

+1-3
Original file line numberDiff line numberDiff line change
@@ -190,10 +190,8 @@ void DwarfStreamer::emitDIE(DIE &Die) {
190190
/// Emit the debug_str section stored in \p Pool.
191191
void DwarfStreamer::emitStrings(const NonRelocatableStringpool &Pool) {
192192
Asm->OutStreamer->SwitchSection(MOFI->getDwarfStrSection());
193-
std::vector<DwarfStringPoolEntryRef> Entries = Pool.getEntries();
193+
std::vector<DwarfStringPoolEntryRef> Entries = Pool.getEntriesForEmission();
194194
for (auto Entry : Entries) {
195-
if (Entry.getIndex() == -1U)
196-
break;
197195
// Emit the string itself.
198196
Asm->OutStreamer->EmitBytes(Entry.getString());
199197
// Emit a null terminator.

‎llvm/tools/dsymutil/MachOUtils.cpp

+2-3
Original file line numberDiff line numberDiff line change
@@ -514,10 +514,9 @@ bool generateDsymCompanion(const DebugMap &DM, MCStreamer &MS,
514514
// Reproduce that behavior for now (there is corresponding code in
515515
// transferSymbol).
516516
OutFile << '\0';
517-
std::vector<DwarfStringPoolEntryRef> Strings = NewStrings.getEntries();
517+
std::vector<DwarfStringPoolEntryRef> Strings =
518+
NewStrings.getEntriesForEmission();
518519
for (auto EntryRef : Strings) {
519-
if (EntryRef.getIndex() == -1U)
520-
break;
521520
OutFile.write(EntryRef.getString().data(),
522521
EntryRef.getString().size() + 1);
523522
}

0 commit comments

Comments
 (0)
Please sign in to comment.