Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 159
0.00% covered (danger)
0.00%
0 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
MathPerformance
0.00% covered (danger)
0.00%
0 / 154
0.00% covered (danger)
0.00%
0 / 12
930
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
12
 actionExport
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 getFormulae
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
12
 actionBenchmark
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
42
 time
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
12
 resetTimer
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 vPrint
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 runTest
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 actionPng
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 processImage
0.00% covered (danger)
0.00%
0 / 40
0.00% covered (danger)
0.00%
0 / 1
20
 makePath
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
1#!/usr/bin/env php
2<?php
3/**
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; either version 2 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License along
15 * with this program; if not, write to the Free Software Foundation, Inc.,
16 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 * http://www.gnu.org/copyleft/gpl.html
18 *
19 * @ingroup Maintenance
20 */
21
22use MediaWiki\Extension\Math\Hooks as MathHooks;
23use MediaWiki\Extension\Math\MathLaTeXML;
24use MediaWiki\Extension\Math\MathMathML;
25use MediaWiki\Extension\Math\MathRestbaseInterface;
26use MediaWiki\Logger\LoggerFactory;
27use MediaWiki\MediaWikiServices;
28
29require_once __DIR__ . '/../../../maintenance/Maintenance.php';
30
31class MathPerformance extends Maintenance {
32
33    /** @var bool */
34    private $verbose;
35    /** @var \Wikimedia\Rdbms\IDatabase */
36    private $db;
37    /** @var string|null */
38    private $currentHash;
39    /** @var float */
40    private $time = 0.0; // microtime( true );
41    /** @var float[] */
42    private $performance = [];
43    /** @var string */
44    private $renderingMode = 'mathml';
45
46    public function __construct() {
47        parent::__construct();
48        $this->addDescription( 'Run math performance tests.' );
49        $this->addArg( 'action', 'Selects what should be done.', false );
50        $this->addArg( 'shares', 'How many pieces should be used.', false );
51        $this->addArg( 'share', 'Which piece should be used. Starting from 0.', false );
52        $this->addOption( 'table', 'table to load the formulae from', false );
53        $this->addOption( 'input', 'field that contains the input', false );
54        $this->addOption( 'hash', 'field that contains the hash', false );
55        $this->addOption( 'min', 'If set, processing is started at formula>min', false );
56        $this->addOption( 'max', 'If set, processing is stopped at formula<=max', false );
57        $this->addOption( 'output', 'The destination of the output defaults to stdout.', false );
58        $this->addOption( 'verbose', 'If set, output for successful rendering will produced', false,
59            false, 'v' );
60        $this->requireExtension( 'MathSearch' );
61    }
62
63    public function execute() {
64        global $wgMathValidModes;
65        $this->db = MediaWikiServices::getInstance()
66            ->getConnectionProvider()
67            ->getPrimaryDatabase();
68        $wgMathValidModes[] = $this->renderingMode;
69        $this->verbose = $this->getOption( 'verbose', false );
70        $this->vPrint( "Loaded." );
71        $action = trim( $this->getArg( 0, 'export' ) );
72        switch ( $action ) {
73            case 'export':
74                $this->actionExport();
75                break;
76            case "benchmark":
77                $this->actionBenchmark();
78                break;
79        }
80        $shareString = $this->getArg( 2, '' );
81        $this->vPrint( "{$shareString}Done." );
82    }
83
84    private function actionExport() {
85        $tex = $this->getOption( 'input', 'math_input' );
86        $hash = $this->getOption( 'hash', 'math_inputhash' );
87        $formulae = $this->getFormulae( $hash, $tex );
88        $out = [];
89        foreach ( $formulae as $formula ) {
90            $out[] = [ $hash => base64_encode( $formula->$hash ), $tex => $formula->$tex ];
91        }
92        $output = $this->getOption( 'output', 'php://stdout' );
93        file_put_contents( $output, json_encode( $out, JSON_PRETTY_PRINT ) );
94    }
95
96    /**
97     * @param string $hash
98     * @param string $tex
99     * @return bool|\Wikimedia\Rdbms\IResultWrapper
100     */
101    private function getFormulae( $hash, $tex ) {
102        $min = $this->getOption( 'min', 0 );
103        $max = $this->getOption( 'max', 0 );
104        $options = [];
105        if ( $max ) {
106            $options['LIMIT'] = $max - $min;
107            $options['OFFSET'] = $min;
108        }
109        $shares = $this->getArg( 1, false ); // 'shares'
110        $share = $this->getArg( 2, 0 ); // 'share'
111        if ( $shares ) {
112            $this->vPrint( "Processing share $share of $shares." );
113            $counts = $this->db->selectField( 'mathlog', 'count(*)' );
114            $bucket = ceil( $counts / $shares );
115            $min = $share * $bucket;
116            $max = $min + $bucket;
117            $options['LIMIT'] = $max - $min;
118            $options['OFFSET'] = $min;
119        }
120        $formulae = $this->db->select(
121            'mathlog',
122            [ $hash, $tex ],
123            '',
124            __METHOD__,
125            $options
126        );
127        return $formulae;
128    }
129
130    private function actionBenchmark() {
131        $tex = $this->getOption( 'input', 'math_input' );
132        $hash = $this->getOption( 'hash', 'math_inputhash' );
133        $formulae = $this->getFormulae( $hash, $tex );
134        foreach ( $formulae as $formula ) {
135            $this->currentHash = $formula->$hash;
136            $rbi = new MathRestbaseInterface( $formula->$tex, false );
137            if ( $this->runTest( $rbi ) ) {
138                if ( round( rand( 0, 1 ) ) ) {
139                    $this->runTest( $rbi, 'getSvg', '1-' ) &&
140                    $this->runTest( $rbi, 'getMathML', '2-' );
141                } else {
142                    $this->runTest( $rbi, 'getMathML', '1-' ) &&
143                    $this->runTest( $rbi, 'getSvg', '2-' );
144                }
145            }
146        }
147    }
148
149    /**
150     * Measures time in ms.
151     * In order to have a formula centric evaluation, we can not just the build in profiler
152     * @param string $category
153     *
154     * @return int
155     */
156    private function time( $category = 'default' ) {
157        global $wgMathDebug;
158        $delta = ( microtime( true ) - $this->time ) * 1000;
159        if ( isset( $this->performance[$category] ) ) {
160            $this->performance[$category] += $delta;
161        } else {
162            $this->performance[$category] = $delta;
163        }
164        $logData = [
165            'math_inputhash'       => $this->currentHash,
166            'mathperformance_name' => substr( $category, 0, 10 ),
167            'mathperformance_time' => $delta,
168            'mathperformance_mode' => MathHooks::mathModeToHashKey( $this->renderingMode )
169        ];
170        if ( $wgMathDebug ) {
171            $this->db->insert( 'mathperformance', $logData );
172        } else {
173            $logData['math_inputhash'] = base64_encode( $logData['math_inputhash'] );
174            echo json_encode( $logData ) . "\n";
175        }
176        $this->resetTimer();
177        return (int)$delta;
178    }
179
180    private function resetTimer() {
181        $this->time = microtime( true );
182    }
183
184    private function vPrint( $string ) {
185        if ( $this->verbose ) {
186            $this->output( $string . "\n" );
187        }
188    }
189
190    private function runTest( MathRestbaseInterface $rbi, $method = 'checkTeX', $prefix = '' ) {
191        try {
192            $this->resetTimer();
193            call_user_func( [ $rbi, $method ] );
194            $this->time( $prefix . $method );
195            return true;
196        } catch ( Exception $e ) {
197            $this->vPrint( "Tex:{$rbi->getTex()}" );
198            $this->vPrint( $e->getMessage() );
199            $this->vPrint( $e->getTraceAsString() );
200            return false;
201        }
202    }
203
204    private function actionPng() {
205        $tex = $this->getOption( 'input', 'math_input' );
206        $hash = $this->getOption( 'hash', 'math_inputhash' );
207        $folder = $this->getOption( 'output', '/tmp/math' );
208        $formulae = $this->getFormulae( $hash, $tex );
209        foreach ( $formulae as $formula ) {
210            $this->currentHash = $formula->$hash;
211            self::processImage( $folder, $formula->$tex );
212        }
213    }
214
215    /**
216     * @param string $folder
217     * @param string $input
218     */
219    private static function processImage( $folder, $input ) {
220        $log = LoggerFactory::getInstance( 'MathSearch-maint' );
221        $mathML = new MathMathML( $input );
222        $md5 = $mathML->getInputHash();
223        $path = self::makePath( $folder, $md5 );
224        $log->debug( 'process image', [
225            'tex' => $input,
226            'path' => $path
227        ] );
228
229        // Mathoid
230        if ( !$mathML->render() ) {
231            $log->error( 'MathML rendering returned false', [
232                'mathml' => $mathML,
233                'tex' => $input,
234                'path' => $path
235            ] );
236            return;
237        }
238        $o = MathObject::cloneFromRenderer( $mathML );
239        file_put_contents( "$path/new.mml", $o->getMathml() );
240        file_put_contents( "$path/new.svg", $o->getSvg() );
241        file_put_contents( "$path/tex.tex", $o->getUserInputTex() );
242
243        // Mathoid eating it's MathML
244        $mathML = new MathMathML( $o->getMathml(), [ 'type' => 'pmml' ] );
245        if ( !$mathML->render() ) {
246            $log->error( 'MathML rendering returned false', [
247                'mathml' => $mathML,
248                'tex' => $input,
249                'path' => $path
250                ] );
251            return;
252        }
253
254        $o = MathObject::cloneFromRenderer( $mathML );
255        file_put_contents( "$path/new-mathml.svg", $o->getSvg( 'force' ) );
256
257        // LaTeXML */
258        $mathML = new MathLaTeXML( $input );
259        if ( !$mathML->readFromCache() ) {
260            $log->error( 'LaTeXML rendering returned false', [
261                'mathml' => $mathML,
262                'tex' => $input,
263                'path' => $path
264            ] );
265            return;
266        }
267
268        $o = MathObject::cloneFromRenderer( $mathML );
269        file_put_contents( "$path/new-latexml.svg", $o->getSvg( 'force' ) );
270        file_put_contents( "$path/new-latexml.mml", $mathML->getMathml() );
271    }
272
273    /**
274     * @param string $folder
275     * @param string $md5
276     * @return string
277     */
278    private static function makePath( $folder, $md5 ) {
279        $subPath = implode( '/', str_split( substr( $md5, 0, 3 ) ) );
280        $path = $folder . '/' . $subPath . '/' . $md5;
281        mkdir( $path, '0755', true );
282        return $path;
283    }
284
285}
286
287$maintClass = MathPerformance::class;
288/** @noinspection PhpIncludeInspection */
289require_once RUN_MAINTENANCE_IF_MAIN;