| 1 | /* |
| 2 | * Copyright (C) 2017 Apple Inc. All rights reserved. |
| 3 | * |
| 4 | * Redistribution and use in source and binary forms, with or without |
| 5 | * modification, are permitted provided that the following conditions |
| 6 | * are met: |
| 7 | * 1. Redistributions of source code must retain the above copyright |
| 8 | * notice, this list of conditions and the following disclaimer. |
| 9 | * 2. Redistributions in binary form must reproduce the above copyright |
| 10 | * notice, this list of conditions and the following disclaimer in the |
| 11 | * documentation and/or other materials provided with the distribution. |
| 12 | * |
| 13 | * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' |
| 14 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, |
| 15 | * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| 16 | * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS |
| 17 | * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
| 18 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
| 19 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
| 20 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
| 21 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
| 22 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF |
| 23 | * THE POSSIBILITY OF SUCH DAMAGE. |
| 24 | */ |
| 25 | |
| 26 | #include "config.h" |
| 27 | #include "SimpleLineLayoutCoverage.h" |
| 28 | |
| 29 | #include "Logging.h" |
| 30 | #include "RenderBlockFlow.h" |
| 31 | #include "RenderChildIterator.h" |
| 32 | #include "RenderStyle.h" |
| 33 | #include "RenderView.h" |
| 34 | #include "Settings.h" |
| 35 | #include "SimpleLineLayout.h" |
| 36 | #include <wtf/text/TextStream.h> |
| 37 | |
| 38 | namespace WebCore { |
| 39 | namespace SimpleLineLayout { |
| 40 | |
| 41 | #ifndef NDEBUG |
| 42 | static void printReason(AvoidanceReason reason, TextStream& stream) |
| 43 | { |
| 44 | switch (reason) { |
| 45 | case FlowIsInsideANonMultiColumnThread: |
| 46 | stream << "flow is inside a non-multicolumn container" ; |
| 47 | break; |
| 48 | case FlowHasHorizonalWritingMode: |
| 49 | stream << "horizontal writing mode" ; |
| 50 | break; |
| 51 | case FlowHasOutline: |
| 52 | stream << "outline" ; |
| 53 | break; |
| 54 | case FlowIsRuby: |
| 55 | stream << "ruby" ; |
| 56 | break; |
| 57 | case FlowHasHangingPunctuation: |
| 58 | stream << "hanging punctuation" ; |
| 59 | break; |
| 60 | case FlowIsPaginated: |
| 61 | stream << "paginated" ; |
| 62 | break; |
| 63 | case FlowHasTextOverflow: |
| 64 | stream << "text-overflow" ; |
| 65 | break; |
| 66 | case FlowIsDepricatedFlexBox: |
| 67 | stream << "depricatedFlexBox" ; |
| 68 | break; |
| 69 | case FlowParentIsPlaceholderElement: |
| 70 | stream << "placeholder element" ; |
| 71 | break; |
| 72 | case FlowParentIsTextAreaWithWrapping: |
| 73 | stream << "wrapping textarea" ; |
| 74 | break; |
| 75 | case FlowHasNonSupportedChild: |
| 76 | stream << "nested renderers" ; |
| 77 | break; |
| 78 | case FlowHasUnsupportedFloat: |
| 79 | stream << "complicated float" ; |
| 80 | break; |
| 81 | case FlowHasUnsupportedUnderlineDecoration: |
| 82 | stream << "text-underline-position: under" ; |
| 83 | break; |
| 84 | case FlowHasJustifiedNonLatinText: |
| 85 | stream << "text-align: justify with non-latin text" ; |
| 86 | break; |
| 87 | case FlowHasOverflowNotVisible: |
| 88 | stream << "overflow: hidden | scroll | auto" ; |
| 89 | break; |
| 90 | case FlowHasWebKitNBSPMode: |
| 91 | stream << "-webkit-nbsp-mode: space" ; |
| 92 | break; |
| 93 | case FlowIsNotLTR: |
| 94 | stream << "dir is not LTR" ; |
| 95 | break; |
| 96 | case FlowHasLineBoxContainProperty: |
| 97 | stream << "line-box-contain value indicates variable line height" ; |
| 98 | break; |
| 99 | case FlowIsNotTopToBottom: |
| 100 | stream << "non top-to-bottom flow" ; |
| 101 | break; |
| 102 | case FlowHasLineBreak: |
| 103 | stream << "line-break property" ; |
| 104 | break; |
| 105 | case FlowHasNonNormalUnicodeBiDi: |
| 106 | stream << "non-normal Unicode bidi" ; |
| 107 | break; |
| 108 | case FlowHasRTLOrdering: |
| 109 | stream << "-webkit-rtl-ordering" ; |
| 110 | break; |
| 111 | case FlowHasLineAlignEdges: |
| 112 | stream << "-webkit-line-align edges" ; |
| 113 | break; |
| 114 | case FlowHasLineSnap: |
| 115 | stream << "-webkit-line-snap property" ; |
| 116 | break; |
| 117 | case FlowHasTextEmphasisFillOrMark: |
| 118 | stream << "text-emphasis (fill/mark)" ; |
| 119 | break; |
| 120 | case FlowHasPseudoFirstLine: |
| 121 | stream << "first-line" ; |
| 122 | break; |
| 123 | case FlowHasPseudoFirstLetter: |
| 124 | stream << "first-letter" ; |
| 125 | break; |
| 126 | case FlowHasTextCombine: |
| 127 | stream << "text combine" ; |
| 128 | break; |
| 129 | case FlowHasTextFillBox: |
| 130 | stream << "background-color (text-fill)" ; |
| 131 | break; |
| 132 | case FlowHasBorderFitLines: |
| 133 | stream << "-webkit-border-fit" ; |
| 134 | break; |
| 135 | case FlowHasNonAutoLineBreak: |
| 136 | stream << "line-break is not auto" ; |
| 137 | break; |
| 138 | case FlowHasSVGFont: |
| 139 | stream << "SVG font" ; |
| 140 | break; |
| 141 | case FlowTextHasSoftHyphen: |
| 142 | stream << "soft hyphen character" ; |
| 143 | break; |
| 144 | case FlowTextHasDirectionCharacter: |
| 145 | stream << "direction character" ; |
| 146 | break; |
| 147 | case FlowIsMissingPrimaryFont: |
| 148 | stream << "missing primary font" ; |
| 149 | break; |
| 150 | case FlowPrimaryFontIsInsufficient: |
| 151 | stream << "missing glyph or glyph needs another font" ; |
| 152 | break; |
| 153 | case FlowTextIsCombineText: |
| 154 | stream << "text is combine" ; |
| 155 | break; |
| 156 | case FlowTextIsRenderCounter: |
| 157 | stream << "unsupported RenderCounter" ; |
| 158 | break; |
| 159 | case FlowTextIsRenderQuote: |
| 160 | stream << "unsupported RenderQuote" ; |
| 161 | break; |
| 162 | case FlowTextIsTextFragment: |
| 163 | stream << "unsupported TextFragment" ; |
| 164 | break; |
| 165 | case FlowTextIsSVGInlineText: |
| 166 | stream << "unsupported SVGInlineText" ; |
| 167 | break; |
| 168 | case FlowHasComplexFontCodePath: |
| 169 | stream << "text with complex font codepath" ; |
| 170 | break; |
| 171 | case FlowHasTextShadow: |
| 172 | stream << "text-shadow" ; |
| 173 | break; |
| 174 | case FlowChildIsSelected: |
| 175 | stream << "selected content" ; |
| 176 | break; |
| 177 | case FlowFontHasOverflowGlyph: |
| 178 | stream << "-webkit-line-box-contain: glyphs with overflowing text." ; |
| 179 | break; |
| 180 | case FlowTextHasSurrogatePair: |
| 181 | stream << "surrogate pair" ; |
| 182 | break; |
| 183 | case MultiColumnFlowIsNotTopLevel: |
| 184 | stream << "non top level column" ; |
| 185 | break; |
| 186 | case MultiColumnFlowHasColumnSpanner: |
| 187 | stream << "column has spanner" ; |
| 188 | break; |
| 189 | case MultiColumnFlowVerticalAlign: |
| 190 | stream << "column with vertical-align != baseline" ; |
| 191 | break; |
| 192 | case MultiColumnFlowIsFloating: |
| 193 | stream << "column with floating objects" ; |
| 194 | break; |
| 195 | case FlowIncludesDocumentMarkers: |
| 196 | stream << "text includes document markers" ; |
| 197 | break; |
| 198 | case FlowTextIsEmpty: |
| 199 | case FlowHasNoChild: |
| 200 | case FlowHasNoParent: |
| 201 | case FeatureIsDisabled: |
| 202 | default: |
| 203 | break; |
| 204 | } |
| 205 | } |
| 206 | |
| 207 | static void printReasons(AvoidanceReasonFlags reasons, TextStream& stream) |
| 208 | { |
| 209 | bool first = true; |
| 210 | for (auto reasonItem = EndOfReasons >> 1; reasonItem != NoReason; reasonItem >>= 1) { |
| 211 | if (!(reasons & reasonItem)) |
| 212 | continue; |
| 213 | stream << (first ? " " : ", " ); |
| 214 | first = false; |
| 215 | printReason(reasonItem, stream); |
| 216 | } |
| 217 | } |
| 218 | |
| 219 | static void printTextForSubtree(const RenderObject& renderer, unsigned& charactersLeft, TextStream& stream) |
| 220 | { |
| 221 | if (!charactersLeft) |
| 222 | return; |
| 223 | if (is<RenderText>(renderer)) { |
| 224 | String text = downcast<RenderText>(renderer).text(); |
| 225 | text = text.stripWhiteSpace(); |
| 226 | unsigned len = std::min(charactersLeft, text.length()); |
| 227 | stream << text.left(len); |
| 228 | charactersLeft -= len; |
| 229 | return; |
| 230 | } |
| 231 | if (!is<RenderElement>(renderer)) |
| 232 | return; |
| 233 | for (const auto* child = downcast<RenderElement>(renderer).firstChild(); child; child = child->nextSibling()) |
| 234 | printTextForSubtree(*child, charactersLeft, stream); |
| 235 | } |
| 236 | |
| 237 | static unsigned textLengthForSubtree(const RenderObject& renderer) |
| 238 | { |
| 239 | if (is<RenderText>(renderer)) |
| 240 | return downcast<RenderText>(renderer).text().length(); |
| 241 | if (!is<RenderElement>(renderer)) |
| 242 | return 0; |
| 243 | unsigned textLength = 0; |
| 244 | for (const auto* child = downcast<RenderElement>(renderer).firstChild(); child; child = child->nextSibling()) |
| 245 | textLength += textLengthForSubtree(*child); |
| 246 | return textLength; |
| 247 | } |
| 248 | |
| 249 | static void collectNonEmptyLeafRenderBlockFlows(const RenderObject& renderer, HashSet<const RenderBlockFlow*>& leafRenderers) |
| 250 | { |
| 251 | if (is<RenderText>(renderer)) { |
| 252 | if (!downcast<RenderText>(renderer).text().length()) |
| 253 | return; |
| 254 | // Find RenderBlockFlow ancestor. |
| 255 | for (const auto* current = renderer.parent(); current; current = current->parent()) { |
| 256 | if (!is<RenderBlockFlow>(current)) |
| 257 | continue; |
| 258 | leafRenderers.add(downcast<RenderBlockFlow>(current)); |
| 259 | break; |
| 260 | } |
| 261 | return; |
| 262 | } |
| 263 | if (!is<RenderElement>(renderer)) |
| 264 | return; |
| 265 | for (const auto* child = downcast<RenderElement>(renderer).firstChild(); child; child = child->nextSibling()) |
| 266 | collectNonEmptyLeafRenderBlockFlows(*child, leafRenderers); |
| 267 | } |
| 268 | |
| 269 | static void collectNonEmptyLeafRenderBlockFlowsForCurrentPage(HashSet<const RenderBlockFlow*>& leafRenderers) |
| 270 | { |
| 271 | for (const auto* document : Document::allDocuments()) { |
| 272 | if (!document->renderView() || document->pageCacheState() != Document::NotInPageCache) |
| 273 | continue; |
| 274 | if (!document->isHTMLDocument() && !document->isXHTMLDocument()) |
| 275 | continue; |
| 276 | collectNonEmptyLeafRenderBlockFlows(*document->renderView(), leafRenderers); |
| 277 | } |
| 278 | } |
| 279 | |
| 280 | void toggleSimpleLineLayout() |
| 281 | { |
| 282 | for (auto* document : Document::allDocuments()) { |
| 283 | auto& settings = document->mutableSettings(); |
| 284 | settings.setSimpleLineLayoutEnabled(!settings.simpleLineLayoutEnabled()); |
| 285 | } |
| 286 | } |
| 287 | |
| 288 | void printSimpleLineLayoutBlockList() |
| 289 | { |
| 290 | HashSet<const RenderBlockFlow*> leafRenderers; |
| 291 | collectNonEmptyLeafRenderBlockFlowsForCurrentPage(leafRenderers); |
| 292 | if (!leafRenderers.size()) { |
| 293 | WTFLogAlways("No text found in this document\n" ); |
| 294 | return; |
| 295 | } |
| 296 | TextStream stream; |
| 297 | stream << "---------------------------------------------------\n" ; |
| 298 | for (const auto* flow : leafRenderers) { |
| 299 | auto reason = canUseForWithReason(*flow, IncludeReasons::All); |
| 300 | if (reason == NoReason) |
| 301 | continue; |
| 302 | unsigned printedLength = 30; |
| 303 | stream << "\"" ; |
| 304 | printTextForSubtree(*flow, printedLength, stream); |
| 305 | for (;printedLength > 0; --printedLength) |
| 306 | stream << " " ; |
| 307 | stream << "\"(" << textLengthForSubtree(*flow) << "):" ; |
| 308 | printReasons(reason, stream); |
| 309 | stream << "\n" ; |
| 310 | } |
| 311 | stream << "---------------------------------------------------\n" ; |
| 312 | WTFLogAlways("%s" , stream.release().utf8().data()); |
| 313 | } |
| 314 | |
| 315 | void printSimpleLineLayoutCoverage() |
| 316 | { |
| 317 | HashSet<const RenderBlockFlow*> leafRenderers; |
| 318 | collectNonEmptyLeafRenderBlockFlowsForCurrentPage(leafRenderers); |
| 319 | if (!leafRenderers.size()) { |
| 320 | WTFLogAlways("No text found in this document\n" ); |
| 321 | return; |
| 322 | } |
| 323 | TextStream stream; |
| 324 | HashMap<AvoidanceReason, unsigned> flowStatistics; |
| 325 | unsigned textLength = 0; |
| 326 | unsigned unsupportedTextLength = 0; |
| 327 | unsigned numberOfUnsupportedLeafBlocks = 0; |
| 328 | unsigned supportedButForcedToLineLayoutTextLength = 0; |
| 329 | unsigned numberOfSupportedButForcedToLineLayoutLeafBlocks = 0; |
| 330 | for (const auto* flow : leafRenderers) { |
| 331 | auto flowLength = textLengthForSubtree(*flow); |
| 332 | textLength += flowLength; |
| 333 | auto reasons = canUseForWithReason(*flow, IncludeReasons::All); |
| 334 | if (reasons == NoReason) { |
| 335 | if (flow->lineLayoutPath() == RenderBlockFlow::ForceLineBoxesPath) { |
| 336 | supportedButForcedToLineLayoutTextLength += flowLength; |
| 337 | ++numberOfSupportedButForcedToLineLayoutLeafBlocks; |
| 338 | } |
| 339 | continue; |
| 340 | } |
| 341 | ++numberOfUnsupportedLeafBlocks; |
| 342 | unsupportedTextLength += flowLength; |
| 343 | for (auto reasonItem = EndOfReasons >> 1; reasonItem != NoReason; reasonItem >>= 1) { |
| 344 | if (!(reasons & reasonItem)) |
| 345 | continue; |
| 346 | auto result = flowStatistics.add(reasonItem, flowLength); |
| 347 | if (!result.isNewEntry) |
| 348 | result.iterator->value += flowLength; |
| 349 | } |
| 350 | } |
| 351 | stream << "---------------------------------------------------\n" ; |
| 352 | stream << "Number of blocks: total(" << leafRenderers.size() << ") non-simple(" << numberOfUnsupportedLeafBlocks << ")\nContent length: total(" << |
| 353 | textLength << ") non-simple(" << unsupportedTextLength << ")\n" ; |
| 354 | for (const auto& reasonEntry : flowStatistics) { |
| 355 | printReason(reasonEntry.key, stream); |
| 356 | stream << ": " << (float)reasonEntry.value / (float)textLength * 100 << "%\n" ; |
| 357 | } |
| 358 | if (supportedButForcedToLineLayoutTextLength) { |
| 359 | stream << "Simple line layout potential coverage: " << (float)(textLength - unsupportedTextLength) / (float)textLength * 100 << "%\n\n" ; |
| 360 | stream << "Simple line layout actual coverage: " << (float)(textLength - unsupportedTextLength - supportedButForcedToLineLayoutTextLength) / (float)textLength * 100 << "%\nForced line layout blocks: " << numberOfSupportedButForcedToLineLayoutLeafBlocks << " content length: " << supportedButForcedToLineLayoutTextLength << "(" << (float)supportedButForcedToLineLayoutTextLength / (float)textLength * 100 << "%)" ; |
| 361 | } else |
| 362 | stream << "Simple line layout coverage: " << (float)(textLength - unsupportedTextLength) / (float)textLength * 100 << "%" ; |
| 363 | stream << "\n---------------------------------------------------\n" ; |
| 364 | WTFLogAlways("%s" , stream.release().utf8().data()); |
| 365 | } |
| 366 | #endif |
| 367 | |
| 368 | } |
| 369 | } |
| 370 | |