Skip to content

Commit

Permalink
Fix some CoreText perf low-hanging fruit (#1558)
Browse files Browse the repository at this point in the history
- Remove NSLayoutManager __lineHasGlyphsAfterIndex(), which is called multiple times per line
    - Instead, directly search for the index of the last visible glyph once, compare against this
    - Was previously about ~10% of the CPU time of [NSLayoutManager __layoutAllText], now negligible

 - DWriteWrapper_CoreText
    - Skip first range of attributes in __DWriteTextLayoutCreate()
        - Was redundant due to the underlaying Format already taking it into account
        - Saves about 8% of CPU time in __DWriteTextLayoutCreate()
    - reserve() ahead of time for glyphRunDescriptionInfo._clusterMap, CTRun->_glyphOrigins, ->_glyphAdvances
        - Was previously about 8~10% of _DWriteCreateFrame()'s CPU time, now negligible

 - Remove DWriteWrapper _GetUserDefaultLocaleName(), don't wrap in a wstring, directly use a wchar_t buffer
    - Performance impact not measured, likely to be fairly small
 - Reduce the number of character buffer copies in _CFStringFromLocalizedString() by 1
    - Saves 20%~30% of CPU time in _CFStringFromLocalizedString()

 - Remove unused/not useful _characters member from CTTypesetter

 - Misc

Related to #1374
  • Loading branch information
ms-jihua authored Dec 16, 2016
1 parent f8c7157 commit c1ddbd9
Show file tree
Hide file tree
Showing 8 changed files with 95 additions and 86 deletions.
41 changes: 19 additions & 22 deletions Frameworks/CoreGraphics/DWriteWrapper.mm
Original file line number Diff line number Diff line change
Expand Up @@ -96,17 +96,10 @@ HRESULT UpdateCollection(const ComPtr<IDWriteFactory>& dwriteFactory, const ComP
return s_userFontCollection;
}

/**
* Helper method to return the user set default locale string.
*
* @return use set locale string as wstring.
*/
std::wstring _GetUserDefaultLocaleName() {
wchar_t localeName[LOCALE_NAME_MAX_LENGTH];
int defaultLocaleSuccess = GetUserDefaultLocaleName(localeName, LOCALE_NAME_MAX_LENGTH);

// If the default locale is returned, find that locale name, otherwise use "en-us".
return std::wstring(defaultLocaleSuccess ? localeName : c_defaultUserLanguage);
// Private helper, wraps around GetUserDefaultLocaleName() and returns a default of "en-us" if it fails
static inline const wchar_t* __GetUserDefaultLocaleName(wchar_t* buf, size_t bufferSize) {
int defaultLocaleSuccess = GetUserDefaultLocaleName(buf, bufferSize);
return defaultLocaleSuccess ? buf : c_defaultUserLanguage;
}

/**
Expand All @@ -122,14 +115,13 @@ CFStringRef _CFStringFromLocalizedString(IDWriteLocalizedStrings* localizedStrin
return nil;
}

// Get the default locale for this user.
std::wstring localeName = _GetUserDefaultLocaleName();
wchar_t localeNameBuf[LOCALE_NAME_MAX_LENGTH];
const wchar_t* localeName = __GetUserDefaultLocaleName(localeNameBuf, LOCALE_NAME_MAX_LENGTH);

uint32_t index = 0;
BOOL exists = FALSE;

// If the default locale is returned, find that locale name, otherwise use "en-us".
RETURN_NULL_IF_FAILED(localizedString->FindLocaleName(localeName.c_str(), &index, &exists));
RETURN_NULL_IF_FAILED(localizedString->FindLocaleName(localeName, &index, &exists));
if (!exists) {
RETURN_NULL_IF_FAILED(localizedString->FindLocaleName(c_defaultUserLanguage, &index, &exists));
}
Expand All @@ -143,13 +135,15 @@ CFStringRef _CFStringFromLocalizedString(IDWriteLocalizedStrings* localizedStrin
uint32_t length = 0;
RETURN_NULL_IF_FAILED(localizedString->GetStringLength(index, &length));

// Get the string.
std::vector<wchar_t> wcharString = std::vector<wchar_t>(length + 1, 0);
RETURN_NULL_IF_FAILED(localizedString->GetString(index, wcharString.data(), length + 1));
// Get the string. Use length + 1 here as GetString requires room for the null terminator.
woc::unique_iw<wchar_t> wcharString(static_cast<wchar_t*>(IwMalloc(sizeof(wchar_t) * (length + 1))));
RETURN_NULL_IF_FAILED(localizedString->GetString(index, wcharString.get(), length + 1));

// Strip out unnecessary null terminator
return (CFStringRef)CFAutorelease(
CFStringCreateWithCharacters(kCFAllocatorDefault, reinterpret_cast<UniChar*>(wcharString.data()), length));
return (CFStringRef)CFAutorelease(CFStringCreateWithCharactersNoCopy(kCFAllocatorDefault,
reinterpret_cast<UniChar*>(wcharString.release()),
length,
kCFAllocatorDefault));
}

/**
Expand Down Expand Up @@ -280,6 +274,9 @@ HRESULT _DWriteCreateTextFormatWithFontNameAndSize(CFStringRef optionalFontName,
userFontInfo = __GetUserFontCollectionHelper()->GetFontPropertiesFromUppercaseFontName(upperFontName);
}

wchar_t localeNameBuf[LOCALE_NAME_MAX_LENGTH];
const wchar_t* localeName = __GetUserDefaultLocaleName(localeNameBuf, LOCALE_NAME_MAX_LENGTH);

if (info) {
RETURN_IF_FAILED(
dwriteFactory->CreateTextFormat(reinterpret_cast<wchar_t*>(Strings::VectorFromCFString(info->familyName.get()).data()),
Expand All @@ -288,7 +285,7 @@ HRESULT _DWriteCreateTextFormatWithFontNameAndSize(CFStringRef optionalFontName,
info->style,
info->stretch,
fontSize,
_GetUserDefaultLocaleName().data(),
localeName,
&textFormat));
}

Expand All @@ -301,7 +298,7 @@ HRESULT _DWriteCreateTextFormatWithFontNameAndSize(CFStringRef optionalFontName,
userFontInfo->style,
userFontInfo->stretch,
fontSize,
_GetUserDefaultLocaleName().data(),
localeName,
&textFormat));
}

Expand Down
2 changes: 1 addition & 1 deletion Frameworks/CoreText/CTFramesetter.mm
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ @implementation _CTFramesetter : NSObject
// Call _DWriteWrapper to get _CTLine object list that makes up this frame
_CTTypesetter* typesetter = static_cast<_CTTypesetter*>(framesetter->_typesetter);
if (range.length == 0L) {
range.length = typesetter->_characters.size() - range.location;
range.length = [typesetter->_string length] - range.location;
}

StrongId<_CTFrame> ret = _DWriteGetFrame(static_cast<CFAttributedStringRef>(typesetter->_attributedString.get()), range, frameRect);
Expand Down
6 changes: 1 addition & 5 deletions Frameworks/CoreText/CTTypesetter.mm
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,6 @@ - (instancetype)initWithAttributedString:(NSAttributedString*)str {
_attributedString = str;
_string = [str string];

// Measure the string
_characters.resize(str.length);
[_string getCharacters:_characters.data()];

return self;
}

Expand Down Expand Up @@ -109,7 +105,7 @@ CFIndex CTTypesetterSuggestLineBreak(CTTypesetterRef typesetter, CFIndex startIn
CFIndex CTTypesetterSuggestLineBreakWithOffset(CTTypesetterRef ts, CFIndex index, double width, double offset) {
_CTTypesetter* typesetter = static_cast<_CTTypesetter*>(ts);
_CTFrame* frame = _DWriteGetFrame(static_cast<CFAttributedStringRef>(typesetter->_attributedString.get()),
CFRangeMake(index, typesetter->_characters.size() - index),
CFRangeMake(index, [typesetter->_string length] - index),
CGRectMake(offset, 0, width, FLT_MAX));
return ([frame->_lines count] > 0) ? static_cast<_CTLine*>([frame->_lines firstObject])->_strRange.length : 0;
}
Expand Down
17 changes: 12 additions & 5 deletions Frameworks/CoreText/DWriteWrapper_CoreText.mm
Original file line number Diff line number Diff line change
Expand Up @@ -265,11 +265,15 @@ static HRESULT __DWriteTextLayoutCreate(CFAttributedStringRef string, CFRange ra
uint32_t incompatibleAttributeFlag = 0;
CFRange attributeRange;

for (CFIndex currentIndex = range.location; currentIndex < rangeEnd; currentIndex += attributeRange.length) {
CTFontRef font =
static_cast<CTFontRef>(CFAttributedStringGetAttribute(string, currentIndex, kCTFontAttributeName, &attributeRange));
// Find the range of the first set of attributes and skip it, since the underlying DWriteTextFormat has already internalized it
// If this first set of attributes lasts the entire range, the below for loop is not executed at all
// attributeRange is populated even if this attribute is not found
CFAttributedStringGetAttribute(string, range.location, kCTFontAttributeName, &attributeRange);

// attributeRange is properly populated even if this attribute is not found
for (CFIndex index = attributeRange.location + attributeRange.length; index < rangeEnd; index += attributeRange.length) {
CTFontRef font = static_cast<CTFontRef>(CFAttributedStringGetAttribute(string, index, kCTFontAttributeName, &attributeRange));

// attributeRange is populated even if this attribute is not found
const DWRITE_TEXT_RANGE dwriteRange = { attributeRange.location, attributeRange.length };
if (font) {
RETURN_IF_FAILED(__DWriteTextLayoutApplyFont(textLayout, font, dwriteRange));
Expand All @@ -282,7 +286,7 @@ static HRESULT __DWriteTextLayoutCreate(CFAttributedStringRef string, CFRange ra
}

CFNumberRef extraKerningRef =
static_cast<CFNumberRef>(CFAttributedStringGetAttribute(string, currentIndex, kCTKernAttributeName, nullptr));
static_cast<CFNumberRef>(CFAttributedStringGetAttribute(string, index, kCTKernAttributeName, nullptr));
if (extraKerningRef) {
RETURN_IF_FAILED(__DWriteTextLayoutApplyExtraKerning(textLayout, typography, extraKerningRef, dwriteRange));
} else {
Expand Down Expand Up @@ -329,6 +333,7 @@ HRESULT STDMETHODCALLTYPE DrawGlyphRun(_In_ void* clientDrawingContext,
_DWriteGlyphRunDescription glyphRunDescriptionInfo;
glyphRunDescriptionInfo._stringLength = glyphRunDescription->stringLength;
glyphRunDescriptionInfo._textPosition = glyphRunDescription->textPosition;
glyphRunDescriptionInfo._clusterMap.reserve(glyphRun->glyphCount);
std::transform(glyphRunDescription->clusterMap,
glyphRunDescription->clusterMap + glyphRun->glyphCount,
std::back_inserter(glyphRunDescriptionInfo._clusterMap),
Expand Down Expand Up @@ -479,6 +484,8 @@ HRESULT STDMETHODCALLTYPE GetPixelsPerDip(_In_opt_ void* clientDrawingContext, _

// TODO::
// This is a temp workaround until we can have actual glyph origins
run->_glyphOrigins.reserve(glyphRunDetails._dwriteGlyphRun[j].glyphCount);
run->_glyphAdvances.reserve(glyphRunDetails._dwriteGlyphRun[j].glyphCount);
for (int index = 0; index < glyphRunDetails._dwriteGlyphRun[j].glyphCount; index++) {
run->_glyphOrigins.emplace_back(CGPoint{ xPos, yPos });
run->_glyphAdvances.emplace_back(CGSize{ glyphRunDetails._dwriteGlyphRun[j].glyphAdvances[index], 0.0f });
Expand Down
109 changes: 62 additions & 47 deletions Frameworks/UIKit/NSLayoutManager.mm
Original file line number Diff line number Diff line change
Expand Up @@ -34,22 +34,38 @@ @implementation NSLayoutManager {
woc::unique_cf<CTFrameRef> _frame;
}

// Returns true if any of the characters in line after index have visible glyphs, false otherwise
// Used to determine if a given index is past the visible part of a line for linebreaking
static bool __lineHasGlyphsAfterIndex(CTLineRef line, CFIndex index) {
for (id runRef in static_cast<NSArray*>(CTLineGetGlyphRuns(line))) {
CTRunRef run = static_cast<CTRunRef>(runRef);
CFRange runRange = CTRunGetStringRange(run);
if (runRange.location <= index && index <= runRange.location + runRange.length) {
const CFIndex* indices = CTRunGetStringIndicesPtr(run);
if (std::any_of(indices,
indices + CTRunGetGlyphCount(run),
std::bind(std::greater_equal<CFIndex>(), std::placeholders::_1, index))) {
return true;
// Private helper:
// Given a CTLineRef, returns the index of the character in the original string, that maps to the last visible glyph in the line
static CFIndex __CTLineGetStringIndexOfLastVisibleGlyph(CTLineRef line) {
CFArrayRef runs = CTLineGetGlyphRuns(line);

CFIndex ret = -1;
for (CFIndex i = CFArrayGetCount(runs) - 1; i >= 0; --i) {
CTRunRef run = static_cast<CTRunRef>(CFArrayGetValueAtIndex(runs, i));
CFIndex glyphCount = CTRunGetGlyphCount(run);

// Open question: Can runs ever be non-monotonic with respect to the original string?
// If not, can do an early return here
if (glyphCount > 0) {
const CFIndex* stringIndices = CTRunGetStringIndicesPtr(run);

// Glyphs within a run can be non-monotonic with respect to the original string
// Find the greatest index within the run's string indices
const CFIndex* maxIndexInRun = std::max_element(stringIndices, stringIndices + glyphCount);

if (*maxIndexInRun > ret) {
ret = *maxIndexInRun;
}
}
}
return false;

return ret;
}

// Private helper for getting the width and height of a line in DIPs
static inline CGSize __CTLineGetBounds(CTLineRef line) {
CGFloat ascent, descent, leading;
return { CTLineGetTypographicBounds(line, &ascent, &descent, &leading), ascent + descent + leading };
}

- (void)__layoutAllText {
Expand All @@ -60,9 +76,6 @@ - (void)__layoutAllText {

woc::unique_cf<CTFramesetterRef> framesetter{ CTFramesetterCreateWithAttributedString(
static_cast<CFAttributedStringRef>(_textStorage.get())) };
NSTextContainer* container = (NSTextContainer*)_textContainers[0];
CGSize containerSize = container.size;
CGPoint origin = { 0, 0 };

// Creates frame containing all of the text, which allows us to measure the line heights for measuring rectangles and breaks the text
// into CTLineRefs for hard line breaks (e.g. '\n') which allows the assumption that once we run out of glyphs in a line to draw we must
Expand All @@ -72,105 +85,107 @@ - (void)__layoutAllText {
_frame.reset(CTFramesetterCreateFrame(framesetter.get(), {}, path.get(), nullptr));
}

NSTextContainer* container = (NSTextContainer*)_textContainers[0];
const CGSize containerSize = container.size;
CGPoint origin{ 0, 0 };

for (id lineRef in static_cast<NSArray*>(CTFrameGetLines(_frame.get()))) {
CTLineRef line = static_cast<CTLineRef>(lineRef);

// Width of line already drawn, saves us from having to redraw line twice
double drawnWidth = 0.0;
// Maximum height of lines on current horizontal, needed to get next yPos
const CGFloat lineHeight = __CTLineGetBounds(line).height;

CGFloat ascent, descent, leading;
const CFRange lineRange = CTLineGetStringRange(line);
const CFIndex lineEnd = lineRange.location + lineRange.length; // String index of last glyph in line

// Width of entire line
double width = CTLineGetTypographicBounds(line, &ascent, &descent, &leading);
const CFIndex indexOfLastVisibleChar = __CTLineGetStringIndexOfLastVisibleGlyph(line); // Alternate termination point

// Maximum height of lines on current horizontal, needed to get next yPos
CGFloat lineHeight = leading + ascent + descent;
CGRect proposedRect{}; // Proposes a drawing rectangle to [NSTextContainer lineFragmentRectForProposedRect]
proposedRect.size.width = containerSize.width;
proposedRect.size.height = lineHeight;

CFRange lineRange = CTLineGetStringRange(line);
CGRect frameDrawRect{}; // Drawing rectangle actually used in CTFramesetterCreateFrame to layout the text
frameDrawRect.size.height = lineHeight;

// Index of first glyph in line to be drawn
CFIndex stringIndex = lineRange.location;
CFIndex lineEnd = stringIndex + lineRange.length;
double drawnWidth = 0.0; // Width of line already drawn, saves us from having to redraw line twice

CFIndex stringIndex = lineRange.location; // String index of first glyph in line to be drawn
while (stringIndex < lineEnd) {
if (origin.y > containerSize.height) {
// Added as much text as can fit in the frame, can end here
return;
}

if (!__lineHasGlyphsAfterIndex(line, stringIndex)) {
// Ended up without any visible glyphs to draw
// Caused by a line of only whitespace
if (stringIndex > indexOfLastVisibleChar) {
// Ended up without any visible glyphs to draw, caused by a line of only whitespace
origin = { 0.0f, origin.y + lineHeight };
_totalSize.height += lineHeight;
break;
}

CGRect remainingRect = {};
CGRect rect = [container lineFragmentRectForProposedRect:CGRectMake(origin.x, origin.y, containerSize.width, lineHeight)
atIndex:stringIndex
writingDirection:NSWritingDirectionLeftToRight
remainingRect:&remainingRect];
CGRect remainingRect{};
proposedRect.origin = origin;
const CGRect rect = [container lineFragmentRectForProposedRect:proposedRect
atIndex:stringIndex
writingDirection:NSWritingDirectionLeftToRight
remainingRect:&remainingRect];

// Approximate how many characters can fit to keep from re-rendering large amounts of text
CFIndex lastIndex = std::min(CTLineGetStringIndexForPosition(line, { drawnWidth + rect.size.width, 0.0f }) + 1, lineEnd);
const CFIndex lastIndex = std::min(CTLineGetStringIndexForPosition(line, { drawnWidth + rect.size.width, 0.0f }) + 1, lineEnd);

if (lastIndex == stringIndex) {
// Unable to fit any text in the current rect, continue to next
if (remainingRect.size.width > 0.0f && stringIndex < lineEnd) {
origin = { remainingRect.origin.x, origin.y };
} else {
origin = { 0.0f, origin.y + lineHeight };
_totalSize.height += lineHeight;
}

continue;
}

woc::unique_cf<CGPathRef> path{ CGPathCreateWithRect(CGRectMake(0, 0, rect.size.width, lineHeight), nullptr) };
frameDrawRect.size.width = rect.size.width;
woc::unique_cf<CGPathRef> path{ CGPathCreateWithRect(frameDrawRect, nullptr) };
woc::unique_cf<CTFrameRef> frame{
CTFramesetterCreateFrame(framesetter.get(), { stringIndex, lastIndex - stringIndex }, path.get(), nullptr)
};

// Create line to fit as much text as possible in given rect
CTLineRef fitLine = static_cast<CTLineRef>(CFArrayGetValueAtIndex(CTFrameGetLines(frame.get()), 0));

CFIndex fitLength = CTLineGetStringRange(fitLine).length;
const CFIndex fitLength = CTLineGetStringRange(fitLine).length;
if (fitLength == 0L) {
// Failed to fit any text in the current rect, continue to next
if (remainingRect.size.width > 0.0f && stringIndex < lineEnd) {
origin = { remainingRect.origin.x, origin.y };
} else {
origin = { 0.0f, origin.y + lineHeight };
_totalSize.height += lineHeight;
}

continue;
}

// Increase index of next character to layout
stringIndex += fitLength;

// Save line and origin for when it is drawn
[_ctLines addObject:(id)fitLine];
_lineOrigins.emplace_back(CGPoint{ rect.origin.x, rect.origin.y + lineHeight });

double fitWidth = CTLineGetTypographicBounds(fitLine, nullptr, nullptr, nullptr);
const double fitWidth = CTLineGetTypographicBounds(fitLine, nullptr, nullptr, nullptr);
drawnWidth += fitWidth;

if (remainingRect.size.width > 0 && stringIndex < lineEnd) {
origin = { remainingRect.origin.x, origin.y };
} else {
origin = { 0.0f, origin.y + lineHeight };
_totalSize.height += lineHeight;
_totalSize.width = std::max(_totalSize.width, rect.origin.x + (CGFloat)fitWidth);
if (!__lineHasGlyphsAfterIndex(line, stringIndex)) {
_totalSize.width = std::max(_totalSize.width, static_cast<CGFloat>(rect.origin.x + fitWidth));
if (stringIndex > indexOfLastVisibleChar) {
// Ended up with whitespace at end of the line, break to next line
break;
}
}
}
}

_totalSize.height = origin.y;
}

- (void)layoutIfNeeded {
Expand Down
4 changes: 0 additions & 4 deletions Frameworks/include/CoreGraphics/DWriteWrapper.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,6 @@
#import <memory>

// General DWrite helpers
#ifdef __cplusplus
extern "C++" std::wstring _GetUserDefaultLocaleName();
#endif

static inline CFStringRef _CFStringCreateUppercaseCopy(CFStringRef string) {
CFMutableStringRef ret = CFStringCreateMutableCopy(nullptr, CFStringGetLength(string), string);
CFStringUppercase(ret, nullptr);
Expand Down
Loading

0 comments on commit c1ddbd9

Please sign in to comment.