mirror of
https://github.com/SpartanJ/eepp.git
synced 2026-09-22 13:01:05 +03:00
Fix HTML loading stalls on large modern pages
- batch asynchronous HTTP stylesheets and apply them in a single reload - skip unsupported block at-rules without leaking nested rules - prevent scripts nested in unknown custom elements from entering the render tree - add regression coverage for unsupported at-rules and nested scripts - document the Ninja release workflow for performance testing
This commit is contained in:
@@ -1,4 +1,23 @@
|
|||||||
# Build Instructions (Debug Mode)
|
# Build Instructions
|
||||||
|
|
||||||
|
The build configurations in `.ecode/project_build.json` are the source of truth for the
|
||||||
|
developer's local build workflows. Check that file before selecting a generator, backend, or
|
||||||
|
build flags. In particular, do not use an AddressSanitizer build to evaluate runtime performance.
|
||||||
|
|
||||||
|
## Release Performance Builds (Linux)
|
||||||
|
|
||||||
|
For performance investigations, use the `eepp-linux-ninja` configuration from
|
||||||
|
`.ecode/project_build.json`. At the time of writing, its commands are:
|
||||||
|
|
||||||
|
`premake5 --disable-static-build --with-debug-symbols --with-backend=SDL3 ninja`
|
||||||
|
|
||||||
|
`ninja -C make/linux release`
|
||||||
|
|
||||||
|
This produces an optimized release build with debug symbols and without AddressSanitizer. Run the
|
||||||
|
release executable (for example, `bin/eepp-ui-html`) when measuring performance. Recheck
|
||||||
|
`.ecode/project_build.json` before use because the local configuration may change.
|
||||||
|
|
||||||
|
## Debug and Unit-Test Builds
|
||||||
|
|
||||||
All build commands must be executed from the **root project directory**. Follow these steps to build the project:
|
All build commands must be executed from the **root project directory**. Follow these steps to build the project:
|
||||||
|
|
||||||
|
|||||||
@@ -958,6 +958,8 @@ class EE_API UISceneNode : public SceneNode {
|
|||||||
bool mIsLoading{ false };
|
bool mIsLoading{ false };
|
||||||
bool mUpdatingLayouts{ false };
|
bool mUpdatingLayouts{ false };
|
||||||
bool mStyleDuringLoad{ false };
|
bool mStyleDuringLoad{ false };
|
||||||
|
Uint32 mPendingHTTPStyleSheetLoads{ 0 };
|
||||||
|
bool mHTTPStyleSheetChanged{ false };
|
||||||
UIThemeManager* mUIThemeManager{ nullptr };
|
UIThemeManager* mUIThemeManager{ nullptr };
|
||||||
UIIconThemeManager* mUIIconThemeManager{ nullptr };
|
UIIconThemeManager* mUIIconThemeManager{ nullptr };
|
||||||
std::vector<Graphics::FontPtr> mFontFaces;
|
std::vector<Graphics::FontPtr> mFontFaces;
|
||||||
@@ -1183,6 +1185,11 @@ class EE_API UISceneNode : public SceneNode {
|
|||||||
*/
|
*/
|
||||||
void loadCSS( URI uri, std::optional<Time> defer, CSS::StyleSheet::SourceOrder sourceOrder );
|
void loadCSS( URI uri, std::optional<Time> defer, CSS::StyleSheet::SourceOrder sourceOrder );
|
||||||
|
|
||||||
|
void combineHTTPStyleSheet( const std::string& css, const std::string& url, URI baseURI,
|
||||||
|
CSS::StyleSheet::SourceOrder sourceOrder );
|
||||||
|
|
||||||
|
void finishHTTPStyleSheetLoad();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Loads glyph icons from @glyph-icon rules.
|
* @brief Loads glyph icons from @glyph-icon rules.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -135,6 +135,17 @@ bool StyleSheetParser::parse( std::string& css, std::vector<std::string>& import
|
|||||||
} else if ( String::startsWith( trimBuf, "@keyframes" ) ||
|
} else if ( String::startsWith( trimBuf, "@keyframes" ) ||
|
||||||
String::startsWith( trimBuf, "@-webkit-keyframes" ) ) {
|
String::startsWith( trimBuf, "@-webkit-keyframes" ) ) {
|
||||||
keyframesParse( css, rs, pos, buffer );
|
keyframesParse( css, rs, pos, buffer );
|
||||||
|
} else if ( !String::startsWith( trimBuf, "@font-face" ) &&
|
||||||
|
!String::startsWith( trimBuf, "@glyph-icon" ) ) {
|
||||||
|
// Unsupported block at-rules must not leak their nested contents into the
|
||||||
|
// top-level rule stream. We cannot preserve their conditional or cascade
|
||||||
|
// semantics, so skip the complete balanced block.
|
||||||
|
std::size_t closePos = String::findCloseBracket( css, pos - 1, '{', '}' );
|
||||||
|
if ( closePos != std::string::npos ) {
|
||||||
|
rs = ReadingSelector;
|
||||||
|
pos = closePos + 1;
|
||||||
|
buffer.clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -797,6 +797,8 @@ UISceneNode* UISceneNode::setWebResourceCache( WebResourceCachePtr cache,
|
|||||||
}
|
}
|
||||||
|
|
||||||
Uint64 UISceneNode::beginDocumentNavigation( const URI& uri ) {
|
Uint64 UISceneNode::beginDocumentNavigation( const URI& uri ) {
|
||||||
|
mPendingHTTPStyleSheetLoads = 0;
|
||||||
|
mHTTPStyleSheetChanged = false;
|
||||||
return mWebResourceCache && mDocumentSessionId
|
return mWebResourceCache && mDocumentSessionId
|
||||||
? mWebResourceCache->beginNavigation( mDocumentSessionId, uri )
|
? mWebResourceCache->beginNavigation( mDocumentSessionId, uri )
|
||||||
: 0;
|
: 0;
|
||||||
@@ -1916,6 +1918,7 @@ void UISceneNode::loadCSS( URI uri, std::optional<Time> defer,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if ( "http" == uri.getScheme() || "https" == uri.getScheme() ) {
|
} else if ( "http" == uri.getScheme() || "https" == uri.getScheme() ) {
|
||||||
|
mPendingHTTPStyleSheetLoads++;
|
||||||
auto resourceState = mAsyncResourceLoadState;
|
auto resourceState = mAsyncResourceLoadState;
|
||||||
Uint64 resourceGeneration =
|
Uint64 resourceGeneration =
|
||||||
resourceState ? resourceState->generation.load( std::memory_order_acquire ) : 0;
|
resourceState ? resourceState->generation.load( std::memory_order_acquire ) : 0;
|
||||||
@@ -1935,12 +1938,14 @@ void UISceneNode::loadCSS( URI uri, std::optional<Time> defer,
|
|||||||
resourceState, resourceGeneration,
|
resourceState, resourceGeneration,
|
||||||
[css = std::move( css ), url, baseURL,
|
[css = std::move( css ), url, baseURL,
|
||||||
sourceOrder]( UISceneNode* scene ) mutable {
|
sourceOrder]( UISceneNode* scene ) mutable {
|
||||||
scene->combineStyleSheet( css, true, String::hash( url ), baseURL,
|
scene->combineHTTPStyleSheet( css, url, baseURL, sourceOrder );
|
||||||
sourceOrder );
|
|
||||||
Log::debug( "UISceneNode::loadCSS: Loaded - %s", url );
|
Log::debug( "UISceneNode::loadCSS: Loaded - %s", url );
|
||||||
} );
|
} );
|
||||||
} else {
|
} else {
|
||||||
Log::debug( "UISceneNode::loadCSS: Failed to load %s - %s", url, result.error );
|
Log::debug( "UISceneNode::loadCSS: Failed to load %s - %s", url, result.error );
|
||||||
|
UISceneNode::runAsyncResourceOnMainThread(
|
||||||
|
resourceState, resourceGeneration,
|
||||||
|
[]( UISceneNode* scene ) { scene->finishHTTPStyleSheetLoad(); } );
|
||||||
}
|
}
|
||||||
} );
|
} );
|
||||||
} else if ( VFS::instance()->fileExists( uri.getPath() ) ) {
|
} else if ( VFS::instance()->fileExists( uri.getPath() ) ) {
|
||||||
@@ -1956,6 +1961,30 @@ void UISceneNode::loadCSS( URI uri, std::optional<Time> defer,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void UISceneNode::combineHTTPStyleSheet( const std::string& css, const std::string& url,
|
||||||
|
URI baseURI, CSS::StyleSheet::SourceOrder sourceOrder ) {
|
||||||
|
CSS::StyleSheetParser parser;
|
||||||
|
parser.setBaseURI( baseURI );
|
||||||
|
if ( parser.loadFromString( css ) ) {
|
||||||
|
parser.getStyleSheet().setMarker( String::hash( url ) );
|
||||||
|
resolveStyleSheetRelativeURLs( parser.getStyleSheet(), baseURI.empty() ? mURI : baseURI );
|
||||||
|
mStyleSheet.combineStyleSheet( parser.getStyleSheet(), sourceOrder );
|
||||||
|
processStyleSheetAtRules( parser.getStyleSheet(), baseURI );
|
||||||
|
mHTTPStyleSheetChanged = true;
|
||||||
|
}
|
||||||
|
finishHTTPStyleSheetLoad();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UISceneNode::finishHTTPStyleSheetLoad() {
|
||||||
|
if ( mPendingHTTPStyleSheetLoads > 0 )
|
||||||
|
mPendingHTTPStyleSheetLoads--;
|
||||||
|
if ( mPendingHTTPStyleSheetLoads == 0 && mHTTPStyleSheetChanged ) {
|
||||||
|
mHTTPStyleSheetChanged = false;
|
||||||
|
updateStyleSheet( true );
|
||||||
|
refreshWebViewDocumentLayoutAfterStyleChange( mRoot );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void UISceneNode::setInternalPixelsSize( const Sizef& size ) {
|
void UISceneNode::setInternalPixelsSize( const Sizef& size ) {
|
||||||
Sizef s( size );
|
Sizef s( size );
|
||||||
if ( s != mSize ) {
|
if ( s != mSize ) {
|
||||||
|
|||||||
@@ -523,6 +523,12 @@ void UITextSpan::loadFromXmlNode( const pugi::xml_node& node ) {
|
|||||||
if ( hasElements ) {
|
if ( hasElements ) {
|
||||||
for ( pugi::xml_node child = node.first_child(); child; child = child.next_sibling() ) {
|
for ( pugi::xml_node child = node.first_child(); child; child = child.next_sibling() ) {
|
||||||
if ( child.type() == pugi::node_element ) {
|
if ( child.type() == pugi::node_element ) {
|
||||||
|
// Unknown HTML elements are represented by UITextSpan. Keep script elements and
|
||||||
|
// their potentially very large data payloads out of the render tree even when
|
||||||
|
// they are nested below one of those custom elements.
|
||||||
|
if ( String::iequals( child.name(), "script" ) )
|
||||||
|
continue;
|
||||||
|
|
||||||
UIWidget* widget = UIWidgetCreator::createFromName( child.name() );
|
UIWidget* widget = UIWidgetCreator::createFromName( child.name() );
|
||||||
|
|
||||||
if ( widget == nullptr )
|
if ( widget == nullptr )
|
||||||
|
|||||||
@@ -3003,6 +3003,36 @@ UTEST( UIHTML, MarkdownViewLoadsBodyChildrenIntoNativeTree ) {
|
|||||||
Engine::destroySingleton();
|
Engine::destroySingleton();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UTEST( UIHTML, ScriptNestedInUnknownElementIsNotRendered ) {
|
||||||
|
init_ui_test();
|
||||||
|
auto* sceneNode = SceneManager::instance()->getUISceneNode();
|
||||||
|
|
||||||
|
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
|
||||||
|
<html><body><react-app id="app">
|
||||||
|
<script type="application/json">{"payload":"must-not-render"}</script>
|
||||||
|
<span id="visible">Visible</span>
|
||||||
|
</react-app></body></html>
|
||||||
|
)html" ) );
|
||||||
|
|
||||||
|
auto* app = sceneNode->getRoot()->find( "app" );
|
||||||
|
ASSERT_TRUE( app != nullptr );
|
||||||
|
EXPECT_TRUE( app->asType<UIWidget>()->findByTag( "script" ) == nullptr );
|
||||||
|
EXPECT_TRUE( app->find( "visible" ) != nullptr );
|
||||||
|
|
||||||
|
Engine::destroySingleton();
|
||||||
|
}
|
||||||
|
|
||||||
|
UTEST( UIHTML, UnsupportedBlockAtRulesDoNotLeakNestedRules ) {
|
||||||
|
StyleSheetParser parser;
|
||||||
|
ASSERT_TRUE( parser.loadFromString( std::string_view{
|
||||||
|
"@layer framework { .layered { color: red; } @supports (display: grid) { .nested { "
|
||||||
|
"display: grid; } } } .visible { color: blue; }" } ) );
|
||||||
|
|
||||||
|
EXPECT_TRUE( parser.getStyleSheet().findStyleFromSelectorName( ".layered" ).empty() );
|
||||||
|
EXPECT_TRUE( parser.getStyleSheet().findStyleFromSelectorName( ".nested" ).empty() );
|
||||||
|
EXPECT_EQ( 1u, parser.getStyleSheet().findStyleFromSelectorName( ".visible" ).size() );
|
||||||
|
}
|
||||||
|
|
||||||
UTEST( UIHTML, StyleSheetSiblingCombinators ) {
|
UTEST( UIHTML, StyleSheetSiblingCombinators ) {
|
||||||
Engine::instance()->createWindow( WindowSettings( 1024, 768, "CSS Sibling Test",
|
Engine::instance()->createWindow( WindowSettings( 1024, 768, "CSS Sibling Test",
|
||||||
WindowStyle::Default, WindowBackend::Default,
|
WindowStyle::Default, WindowBackend::Default,
|
||||||
|
|||||||
Reference in New Issue
Block a user