Add WAN Cache for native MathML rendering

* Cache results for checked tex and MathML string in one cache.
* Remove access to parsetree
* Introduce run method to speed up service wiring

Note that the indirection table used in previous versions was
abandoned here. texvc does only little unification's of the
input string so that it is not expected that the overall savings
in space and compute time warrant the additional table.

Change-Id: Ib9ce3d2ab02bd9a2a0f9926db6b937435b7e5458
This commit is contained in:
Moritz Schubotz (physikerwelt) 2023-06-19 22:14:14 +02:00
parent ceea8068d0
commit 16d1fdacf4
No known key found for this signature in database
GPG key ID: F803DB146DDF36C3
5 changed files with 137 additions and 57 deletions

View file

@ -88,6 +88,7 @@ class InputCheckFactory {
*/
public function newLocalChecker( string $input, string $type ): LocalChecker {
return new LocalChecker(
$this->cache,
$input,
$type
);

View file

@ -3,32 +3,54 @@
namespace MediaWiki\Extension\Math\InputCheck;
use Exception;
use MediaWiki\Extension\Math\TexVC\Nodes\TexArray;
use MediaWiki\Extension\Math\TexVC\TexVC;
use Message;
use WANObjectCache;
class LocalChecker extends BaseChecker {
public const VERSION = 1;
private const VALID_TYPES = [ 'tex', 'inline-tex', 'chem' ];
private ?Message $error = null;
private ?TexArray $parseTree = null;
private ?string $mathMl = null;
/**
* @param string $tex the TeX input string to be checked
* @param string $type the input type
*/
public function __construct( $tex = '', string $type = 'tex' ) {
if ( !in_array( $type, self::VALID_TYPES, true ) ) {
$this->error = $this->errorObjectToMessage(
(object)[ "error" => "Unsupported type passed to LocalChecker: " . $type ], "LocalCheck" );
private string $type;
private WANObjectCache $cache;
private bool $isChecked = false;
public function __construct( WANObjectCache $cache, $tex = '', string $type = 'tex' ) {
$this->cache = $cache;
parent::__construct( $tex );
$this->type = $type;
}
public function isValid(): bool {
$this->run();
return parent::isValid();
}
public function getValidTex(): ?string {
$this->run();
return parent::getValidTex();
}
public function run() {
if ( $this->isChecked ) {
return;
}
if ( !in_array( $this->type, self::VALID_TYPES, true ) ) {
$this->error = $this->errorObjectToMessage(
(object)[ "error" => "Unsupported type passed to LocalChecker: " . $this->type ], "LocalCheck" );
return;
}
parent::__construct( $tex );
$options = $type === 'chem' ? [ "usemhchem" => true ] : null;
try {
$result = ( new TexVC() )->check( $tex, $options );
$result = $this->cache->getWithSetCallback(
$this->getInputCacheKey(),
WANObjectCache::TTL_INDEFINITE,
[ $this, 'runCheck' ],
[ 'version' => self::VERSION ],
);
} catch ( Exception $e ) { // @codeCoverageIgnoreStart
// This is impossible since errors are thrown only if the option debug would be set.
$this->error = Message::newFromKey( 'math_failure' );
@ -38,12 +60,13 @@ class LocalChecker extends BaseChecker {
if ( $result['status'] === '+' ) {
$this->isValid = true;
$this->validTeX = $result['output'];
$this->parseTree = $result['input'];
$this->mathMl = $result['mathml'];
} else {
$this->error = $this->errorObjectToMessage(
(object)[ "error" => (object)$result["error"] ],
"LocalCheck" );
}
$this->isChecked = true;
}
/**
@ -51,15 +74,44 @@ class LocalChecker extends BaseChecker {
* @return ?Message
*/
public function getError(): ?Message {
$this->run();
return $this->error;
}
public function getParseTree(): ?TexArray {
return $this->parseTree;
}
public function getPresentationMathMLFragment(): string {
$this->mathMl ??= $this->parseTree->renderMML();
public function getPresentationMathMLFragment(): ?string {
$this->run();
return $this->mathMl;
}
public function getInputCacheKey(): string {
return $this->cache->makeGlobalKey(
self::class,
md5( $this->type . '-' . $this->inputTeX )
);
}
public function runCheck(): array {
$options = $this->type === 'chem' ? [ "usemhchem" => true ] : null;
try {
$result = ( new TexVC() )->check( $this->inputTeX, $options );
} catch ( Exception $e ) { // @codeCoverageIgnoreStart
// This is impossible since errors are thrown only if the option debug would be set.
$this->error = Message::newFromKey( 'math_failure' );
return [];
// @codeCoverageIgnoreEnd
}
if ( $result['status'] === '+' ) {
return [
'status' => '+',
'output' => $result['output'],
'mathml' => $result['input']->renderMML()
];
} else {
return [
'status' => $result['status'],
'error' => $result['error'],
];
}
}
}

View file

@ -45,7 +45,7 @@ class MathNativeMML extends MathMathML {
}
$root = new MMLmath( "", $attributes );
$this->setMathml( $root->encapsulateRaw( $presentation ) );
$this->setMathml( $root->encapsulateRaw( $presentation ?? '' ) );
return StatusValue::newGood();
}

View file

@ -50,8 +50,17 @@ class InputCheckFactoryTest extends MediaWikiIntegrationTestCase {
}
public function testInvalidLocalChecker() {
$checker = $this->newServiceInstance( InputCheckFactory::class, [] )
->newLocalChecker( 'FORMULA', 'INVALIDTYPE' );
$myFactory = new InputCheckFactory(
new ServiceOptions( InputCheckFactory::CONSTRUCTOR_OPTIONS, [
'MathMathMLUrl' => 'something',
'MathTexVCService' => 'local',
'MathLaTeXMLTimeout' => 240
] ),
$this->fakeWAN,
$this->fakeHTTP,
LoggerFactory::getInstance( 'Math' )
);
$checker = $myFactory->newLocalChecker( 'FORMULA', 'INVALIDTYPE' );
$this->assertInstanceOf( LocalChecker::class, $checker );
$this->assertInstanceOf( Message::class, $checker->getError() );
$this->assertFalse( $checker->isValid() );

View file

@ -2,9 +2,10 @@
namespace MediaWiki\Extension\Math\InputCheck;
use MediaWiki\Extension\Math\TexVC\Nodes\TexArray;
use HashBagOStuff;
use MediaWikiIntegrationTestCase;
use Message;
use WANObjectCache;
/**
* @group Math
@ -13,8 +14,12 @@ use Message;
* @covers \MediaWiki\Extension\Math\InputCheck\LocalChecker
*/
class LocalCheckerTest extends MediaWikiIntegrationTestCase {
private const SAMPLE_KEY =
'global:MediaWiki\Extension\Math\InputCheck\LocalChecker:d5f40adbd26ff8b19b2c33289d7334b6';
public function testValid() {
$checker = new LocalChecker( '\sin x^2' );
$checker = new LocalChecker( WANObjectCache::newEmpty(), '\sin x^2' );
$this->assertNull( $checker->getError() );
$this->assertTrue( $checker->isValid() );
$this->assertNull( $checker->getError() );
@ -22,30 +27,30 @@ class LocalCheckerTest extends MediaWikiIntegrationTestCase {
}
public function testValidTypeTex() {
$checker = new LocalChecker( '\sin x^2', 'tex' );
$checker = new LocalChecker( WANObjectCache::newEmpty(), '\sin x^2', 'tex' );
$this->assertTrue( $checker->isValid() );
}
public function testValidTypeChem() {
$checker = new LocalChecker( '{\\displaystyle {\\ce {\\cdot OHNO_{2}}}}', 'chem' );
$checker = new LocalChecker( WANObjectCache::newEmpty(), '{\\displaystyle {\\ce {\\cdot OHNO_{2}}}}', 'chem' );
$this->assertTrue( $checker->isValid() );
}
public function testValidTypeInline() {
$checker = new LocalChecker( '{\\textstyle \\log2 }', 'inline-tex' );
$checker = new LocalChecker( WANObjectCache::newEmpty(), '{\\textstyle \\log2 }', 'inline-tex' );
$this->assertTrue( $checker->isValid() );
}
public function testInvalidType() {
$checker = new LocalChecker( '\sin x^2', 'INVALIDTYPE' );
$checker = new LocalChecker( WANObjectCache::newEmpty(), '\sin x^2', 'INVALIDTYPE' );
$this->assertInstanceOf( LocalChecker::class, $checker );
$this->assertInstanceOf( Message::class, $checker->getError() );
$this->assertFalse( $checker->isValid() );
$this->assertNull( $checker->getParseTree() );
$this->assertNull( $checker->getPresentationMathMLFragment() );
}
public function testInvalid() {
$checker = new LocalChecker( '\sin\newcommand' );
$checker = new LocalChecker( WANObjectCache::newEmpty(), '\sin\newcommand' );
$this->assertFalse( $checker->isValid() );
$this->assertStringContainsString(
@ -61,7 +66,7 @@ class LocalCheckerTest extends MediaWikiIntegrationTestCase {
}
public function testErrorSyntax() {
$checker = new LocalChecker( '\left(' );
$checker = new LocalChecker( WANObjectCache::newEmpty(), '\left(' );
$this->assertFalse( $checker->isValid() );
$this->assertStringContainsString(
Message::newFromKey( 'math_syntax_error' )
@ -73,37 +78,50 @@ class LocalCheckerTest extends MediaWikiIntegrationTestCase {
);
}
public function testGetParseTree() {
$checker = new LocalChecker( 'e^{i \pi} + 1 = 0' );
$this->assertTrue( $checker->isValid() );
$parseTree = $checker->getParseTree();
$this->assertInstanceOf( TexArray::class, $parseTree );
$this->assertEquals( 5, $parseTree->getLength() );
}
public function testGetParseTreeNull() {
$checker = new LocalChecker( '\invalid' );
$this->assertFalse( $checker->isValid() );
$this->assertNull( $checker->getParseTree() );
}
public function testGetParseTreeEmpty() {
$checker = new LocalChecker( '' );
$this->assertTrue( $checker->isValid() );
$parseTree = $checker->getParseTree();
$this->assertInstanceOf( TexArray::class, $parseTree );
$this->assertSame( 0, $parseTree->getLength() );
}
public function testGetMML() {
$checker = new LocalChecker( 'e^{i \pi} + 1 = 0' );
$checker = new LocalChecker( WANObjectCache::newEmpty(), 'e^{i \pi} + 1 = 0' );
$mml = $checker->getPresentationMathMLFragment();
$this->assertStringContainsString( '<mn>0</mn>', $mml );
}
public function testGetMMLEmpty() {
$checker = new LocalChecker( '' );
$checker = new LocalChecker( WANObjectCache::newEmpty(), '' );
$mml = $checker->getPresentationMathMLFragment();
$this->assertSame( '', $mml );
}
/**
* @covers \MediaWiki\Extension\Math\InputCheck\LocalChecker::getInputCacheKey
*/
public function testGetKey() {
$checker = new LocalChecker( WANObjectCache::newEmpty(), '\sin x^2', 'tex' );
$this->assertSame( self::SAMPLE_KEY, $checker->getInputCacheKey() );
}
public function testCache() {
$fakeWAN = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
$fakeContent = [ 'status' => '+', 'output' => 'out', 'mathml' => 'mml' ];
$fakeWAN->set( self::SAMPLE_KEY,
$fakeContent,
WANObjectCache::TTL_INDEFINITE,
[ 'version' => LocalChecker::VERSION ] );
$checker = new LocalChecker( $fakeWAN, '\sin x^2', 'tex' );
$this->assertSame( $fakeContent['output'], $checker->getValidTex() );
$this->assertSame( $fakeContent['mathml'], $checker->getPresentationMathMLFragment() );
$this->assertSame( true, $checker->isValid() );
}
/**
* @covers \MediaWiki\Extension\Math\InputCheck\LocalChecker::runCheck
*/
public function testRunChecks() {
$fakeContent = [
'status' => '+',
'mathml' => '<mi>sin</mi><mo></mo><msup><mi>x</mi><mrow data-mjx-texclass="ORD"><mn>2</mn></mrow></msup>',
'output' => '\\sin x^{2}'
];
$checker = new LocalChecker( WANObjectCache::newEmpty(), '\sin x^2', 'tex' );
$actual = $checker->runCheck();
$this->assertArrayEquals( $fakeContent, $actual );
}
}