Electronアプリの起動時間短縮に挑む

アプリが起動するまでの時間が短いと気持ちいい

アプリの起動は最初の一回しか発生しないのに、
その後はアプリは起動しっぱなしなのに、
この最初の起動までの時間が短いと、アプリの使用感が非常に良くなるような気がする。 気がしない?


この記事はElectronアプリの起動速度を速くするために
いろいろやったことと、その結果の記録です。
環境はMacのElectronです。


なお計測はしていたが、記録はない。
各仕組みの導入の前後で結構コードが変わってしまって、
計測の仕方も変わってしまって、
あとバイナリは残っているから調査可能だけど、面倒くさいからであります。


初回起動時の記録は捨てとこう

ところでElectronのアプリって、
ビルド後初回の起動より、2回目・3回目の方が早く起動しますね。
計測して起動時パフォーマンスチューニングするなら、初回の記録は捨てときましょ。


やったことリスト

  • require文の書き換え・遅延ローディング
  • ファイルの統合・分割・minify
  • コールバック・Promise
  • 設定ファイルを同期・非同期で読み込む
  • 設定ファイルの読み込みのタイミングをずらす
  • Electronのバージョンを変更する
  • アプリ起動時の見た目をごまかす


require文の書き換え・遅延ローディング

require文を書き換えて、ライブラリを遅延ローディングするように変更する。
すごく効く。効いた。

var log = require('electron-log');
log.error('error found.');

↓例えば、こう書き換える

var _log, log = () => { _log = _log || require('electron-log'); return _log; };
log().error('error found.');


ファイルの分割・統合・minify

Electronアプリで使用しているファイルを、

  • Webpackなどで1つにまとめたり、
  • 逆に細かく分割してファイル1つあたりを軽量にしたり、
  • ファイルをminifyしてファイルのサイズを小さくしたり。

asarに固めたElectronアプリでは効かない

全然全然本当にまったく効果なかった。
まったく効果がないので、余計な手間がビルド時に発生する分、マイナスまである。

何度となく諦めきれずに試したが、そのたびにまったく効果がないと知ることとなった。
でも、私はきっとまた試すよ。


コールバック・Promise

コールバックとPromiseは、コールバックの方が早い。
世間の流れと逆方向でありますね。

でも、仕方ない。
アプリの起動時には、ほんの僅かでも速度が欲しいのであります。
効く

// Promise
hoge().then((data1) => {
  foo(data1).then(data2) => {
    // 何かやる
  });
});
// コールバック
hoge((data1) => {
  foo(data1, (data2) => {
    // 何かやる
  });
});


設定ファイルを同期・非同期で読み込む

設定ファイルを読み込む処理があるとして、

  • 非同期で設定ファイルを読み込むより、
  • 同期で設定ファイルを読み込む方がかなり早い


早いが、もちろんデメリットあり。
アプリの処理によっては、処理がブロックされて逆に遅くなるでしょう。

更に更に、
例えば、設定ファイルがJSONであるなら、
こんな感じにrequire文を使って読み込むと、
同期での設定ファイル読み込みより更に早かったのですけど、
これはセキュリティ的な問題があるでしょうな。

var dataJson = null;
try {
  // data.jsonの読み込み
  dataJson = require(`${app.getPath('userData')}/data.json`);
} catch (e) {
  dataJson = [];
}
// 読み込み終わったらキャッシュ消しておく
delete require.cache[`${app.getPath('userData')}/data.json`];


設定ファイルの読み込みのタイミングをずらす

私はAngularを使用しているのですけど、
Renderer Processで画面を描画中に設定ファイルを読み込もうとすると、
同期であろうと非同期であろうと、ものすごく遅くなる。

その描画中の範囲を外して設定ファイルを読み込むようにすると
めちゃくちゃ早くなる。
とても良く効く


自分の環境では、同じ処理が実行タイミングの違いで、これくらい効いた。

1秒 → 0.002秒


Electronのバージョンを変更する

少し効く
Electronのバージョンを3から4にする。
何度も計測した結果、ちょっぴり早くなっていると思う。
ちょっぴり。

でも、Electronのバージョンは、あまり選択可能な要素ではないですね。

私はまだElectron4で安定動作させられなかったので、
Electron3に戻しちゃいましたけど、
いろいろ問題解決したら、もちろんElectronのバージョンを上げておきたい。

もうElectronの5が出てる?気のせいでしょ。


アプリ起動時の見た目をごまかす

設定ファイルからデータを読み込んで、 読み込み終わった順にそのデータでビューを更新すると、
画面がちらちら更新されて、体感、アプリの起動が遅く感じる。

一括でパッと表示した方が早く感じる。操作できるまでの時間変わらなくても。

設定ファイルの読み込み順と、読み込みタイミングの考慮は要るけれど、
手間暇かけると、アプリの起動時間の体感は良くなる。


おわり

おわりだよー。

gulp3のrun-sequence + エラー処理なコードを、gulp4で置き換える

いろいろ

gulp3にタスクを連続で実行するrun-sequenceというライブラリがありました。

gulpのバージョン4以降では、このrun-sequenceの代わりに、gulp.seriesを使うことになったのだが、 gulp.seriesには、run-sequenceにあった、発生したエラーを受け取るcallbackが用意されていませんでした。

try-catch文を使っても、gulp.series内で発生したエラーを拾うことはできない。
では、gulp3 → gulp4と書き換える際、run-sequenceをどのように書き換えるべきか。


gulp3 + run-sequenceのコード

run-sequenceでは、こんな感じで発生したエラーを拾えていた。

const gulp = require('gulp');
const runSequence = require('run-sequence');

gulp.task('package', (cb) => {
  runSequence('tsc-debug', '_rm-package', '_package-debug', (err) => {
    if (err) {
      gulp.start('_notifyError');
    }
    cb(err);
  });
});


gulp4 + gulp.seriesのコード

エラーを拾うコールバックがgulp.seriesに用意されていないので、
すんなりgulp4に書き換えられない。 エラー処理が抜けてしまう。

const gulp = require('gulp');

gulp.task('package',
  gulp.series('tsc-debug', '_rm-package', '_package-debug'));


gulp.seriesのコードを追ってみる

gulpの各タスクの処理はtaskWrapperで包まれる。
// node_modules/undertaker/lib/set-task.js
function taskWrapper() {
  return fn.apply(this, arguments);
}
gulp.seriesを作成時、createExtensionsが呼ばれる。
// node_modules/undertaker/lib/series.js
function series() {
  var create = this._settle ? bach.settleSeries : bach.series;
  var args = normalizeArgs(this._registry, arguments);
  var extensions = createExtensions(this); // ←←←←←←←←←←←←←←←←←←←
  var fn = create(args, extensions);
createExtensionsで、エラー処理のハンドリングが実装されている
  • エラー発生時にerrorイベントが発行される
// node_modules/undertaker/lib/helpers/createExtensions.js
function createExtensions(ee) {
  return {
    error: function(error, storage) {
      if (Array.isArray(error)) {
        error = error[0];
      }
      storage.release();
      ee.emit('error', { // ←←←←←←←←←←←←←←←←←←←
        uid: storage.uid,
        name: storage.name,
        branch: storage.branch,
        error: error,
        duration: process.hrtime(storage.startHr),
        time: Date.now(),
      });
    },
最終的にgulpでエラーを拾える
  • つまり、gulp.series内で発生したエラーは、次のようなコードで拾える。
    • gulp.on('error', エラー処理)
const gulp = require('gulp');

gulp.on('error', (err) => { // ←←←←←←←←←←←←←←←←←←←
  console.log('--- catch error');
});


gulp.seriesで発生したエラーを拾うコードの例(全体)

const gulp = require('gulp');

gulp.task('hello', (cb) => {
  console.log('hello');
  return cb();
});

gulp.task('world', (cb) => {
  console.log('world');
  return cb();
});

gulp.task('gulp', (cb) => {
  console.log('gulp');
  throw new Error('error from task "gulp"');
});

gulp.on('error', (err) => {
  console.log('--- catch error');
});

gulp.task('run', gulp.series('hello', 'world', 'gulp'));


npmのモジュールにしてみた

  • gulp3 run-sequenceの処理を、なるべくそのままgulp4.seriesに置き換えれるようにするnpmモジュール
npm install --save-dev gulp-task-error-handler


gulp3 run-sequenceのコードを、gulp4 gulp.seriesで置き換える例
  • gulp3 run-sequence code
    • 例えば、このようなコードがあったとして、
const gulp = require('gulp');
const runSequence = require('run-sequence');

gulp.task('package', (cb) => {
  runSequence('tsc-debug', '_rm-package', '_package-debug', (err) => {
    if (err) {
      gulp.start('_notifyError');
    }
    cb(err);
  });
});
  • replace with gulp-task-error-handler sample.
    • このモジュールを使うと、このように置き換えられる。
const gulp = require('gulp');
const handleError = require('gulp-task-error-handler');

gulp.task('package',
  handleError(gulp.series('tsc-debug', '_rm-package', '_package-debug'), (err) => {
    gulp.task('_notifyError')();
  })
);


おわり

おわりなのだ。

MacのElectronで"safeDialogs"の機能が動作するようになった

要約

動作しないと自分の中で評判だった、
Electronの無限アラート防止機能"safeDialogs"の問題が修正。
Electronバージョン5から動作するようになった。


BrowserWindowのsafeDialogsオプション

ElectronのBrowserWindowに、"safeDialogs"というオプションがあります。
このオプションは、例えば、
アラートダイアログが無限に繰り返し表示されるような処理が見つかった時、
そのループを停止してくれる機能です。

safeDialogs Boolean (optional)
- Whether to enable browser style consecutive dialog protection. Default is false.

https://electronjs.org/docs/api/browser-window


動作サンプル


こんな感じの無限アラートを、
Electronのrendererプロセスに仕込んで、

while (true) alert('!')


BrowserWindowのパラメータで、"safeDialogs"を指定しておくと、

function createWindow () {
  mainWindow = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      safeDialogs: true, // ←←←←←←←←←←←←
      safeDialogsMessage: 'stop dialog',
      nodeIntegration: true
    }
  })


無限アラートを検知した時に、
ループを停止するチェックボックスが表示されます。

ただし、Macでは長らく動作していなかった。

この機能、オプション導入直後から、
MacのElectronでは長らく動作していませんでした。
誰も試してなかったのかな。

無限アラートがあっても、スルーされてました。
https://github.com/electron/electron/issues/17543


これで安心

たとえ、兵庫県に住んでいてもね!!

続・Final Cut Pro XでWAVファイルにロールを自動割り当てする機能の仕様を特定した(してない)

要約

ぜんぜん特定してなかった(´д`)


前回
Final Cut Pro XでWAVファイルにロールを自動割り当てする機能の仕様を特定した https://taku-o.hatenablog.jp/entry/2019/05/25/060055


判明した問題

  • mixdownなWAVファイル、
  • ステレオなWAVファイル、

つまり、複数のチャネルを持ったWAVファイルに、
前回、「これで良いんだよ」と紹介した構造の、
次のようなiXMLチャンクを追加すると、

<BWFXML>
    <IXML_VERSION>1.27</IXML_VERSION>
    <TRACK_LIST>
        <TRACK_COUNT>1</TRACK_COUNT>
        <TRACK>
            <CHANNEL_INDEX>1</CHANNEL_INDEX>
            <INTERLEAVE_INDEX>1</INTERLEAVE_INDEX>
            <NAME>dummy-track</NAME>
        </TRACK>
    </TRACK_LIST>
</BWFXML>


Final Cut Pro XにWAVファイルを取り込んだ時に、こうなってしまう。
空のオーディオロールが振られてしまうことが判明したのです。

<空白>
dummy-track


でもでも?

しかしながら、ではこうしたらと、
次のように、WAVファイルのiXMLチャンクに、
チャネル数のトラック情報を入れたとしても、

<BWFXML>
    <IXML_VERSION>1.27</IXML_VERSION>
    <TRACK_LIST>
        <TRACK_COUNT>2</TRACK_COUNT>
        <TRACK>
            <CHANNEL_INDEX>1</CHANNEL_INDEX>
            <INTERLEAVE_INDEX>1</INTERLEAVE_INDEX>
            <NAME>dummy-track</NAME>
        </TRACK>
        <TRACK>
            <CHANNEL_INDEX>2</CHANNEL_INDEX>
            <INTERLEAVE_INDEX>2</INTERLEAVE_INDEX>
            <NAME>dummy-track</NAME>
        </TRACK>
    </TRACK_LIST>
</BWFXML>


今度は、このように無駄なナンバリングサフィックスのついた、
オーディオロールが振られてしまうようなのです。

dummy-track-1
dummy-track-2


この場合、できれば、
Final Cut Pro X上で、手動でオーディオロールを設定した時と同じように、
dummy-trackという名前でロールを1つ設定して欲しいのですね。
そう、手動でなら設定できる。


iXMLチャンクでWAVファイルのオーディオロールの情報を設定しようとした時、
うまく設定できるiXMLのデータ構造がどうにも分からない。


結論

どうすれば良いか、わからぬ・・・
わからぬ・・・

Final Cut Pro XでWAVファイルにロールを自動割り当てする機能の仕様を特定した

概要

Final Cut Pro Xには、オーディオファイルを読み込んだ時に、オーディオロールを自動的に設定する機能があります。

なのですが、その仕様の詳細がよくわからなかった。

このたび、試行錯誤の末、なんとか動作する仕様を特定したので、
まとめておきます。

Final Cut Pro X: オーディオレーンを使ってタイムラインを整理する https://support.apple.com/kb/PH12652

Final Cut Pro X: 読み込み時に割り当てられるオーディオロールを設定する https://support.apple.com/kb/PH26179


特定した仕様

iXML Chunkの構造

まず、iXML Chunkの定義。
次のようなChunkの構造を、wavファイルの最後に突っ込む。
wavファイルに含まれるRIFF Chunkにはファイルサイズの項目があるから、既存のwavファイルにiXML Chunkを追加する時は、そちらのサイズも増やさないとダメです。

bytes 定義 入れるデータ
1-4 Chunk ID iXML
5-8 Chunk Size iXML Chunkの長さ
9- iXML Meta Data iXMLメタデータ


iXMLメタデータ

Final Cut Pro Xで動作する、最小のiXMLメタデータの定義は次の通りです。
BWFXML/TRACK_LIST/TRACK/NAMEの位置(audio role nameと入っている場所)で、自動割り当てしたいオーディオロール名を指定します。
("&"、"<"、">"を値に使う場合はエスケープが必要。)

<?xml version="1.0" encoding="UTF-8"?>
<BWFXML>
    <IXML_VERSION>1.27</IXML_VERSION>
    <TRACK_LIST>
        <TRACK_COUNT>1</TRACK_COUNT>
        <TRACK>
            <CHANNEL_INDEX>1</CHANNEL_INDEX>
            <INTERLEAVE_INDEX>1</INTERLEAVE_INDEX>
            <NAME>audio role name</NAME>
        </TRACK>
    </TRACK_LIST>
</BWFXML>


拡張子

確認した限りでは、ファイルの拡張子は.wavでないと動作しない。
正直、別の拡張子を当てたかった(-_-;)
.bwfとか。


割り当てられるオーディオロール

ドラッグアンドドロップで、wavファイルをインポートする場合、
audio subroleはiXMLチャンクで指定できますが、親のオーディオロールは
指定できない(断言)
デフォルトでは、ダイアログ(dialogue)の子ロールに割り当てられてしまう。


これを変えたい場合は、

  • メディアの読み込み機能からインポート時にロールを指定するか、
  • 環境設定でデフォルトで割り当てるロールを変更する、と良い。


ロールの編集で、ロールをドラッグすると、ロールどうしをマージできるので、
一度、取り込んでから、正しいロールに統合するのでも良いとは思います。
でも、一時でも、ダイアログ(dialogue)に割り当てられちゃうと面倒くさい使いづらい


その他、やったこと

まず、npmモジュールを作ってみた

npmモジュールを作った。
このFinal Cut Pro X用の、iXMLチャンクをwavファイルに追加できる。 https://www.npmjs.com/package/fcpx-audio-role-encoder


フォルダアクションでwavファイルを変換

作ったnpmモジュールを利用して、
wavファイルを変換するAutoMatorのアクションと、
フォルダに入れられたwavファイルを変換処理するフォルダアクションを作った。
https://github.com/taku-o/fcpx-audio-role-workflow


これで、フォルダに音声ファイルを突っ込むだけで、
ファイル名のパターンに従って、
オーディオロールが自動設定されるようになったのである。
めでたしめでたし。


なんだか、めずらしく役に立つ話、した気がする!!!

AquesTalkの機能をJavaScriptから呼び出す機能をライブラリに切り出した

概要

Mac用の動画作成用ゆっくりボイスアプリ MYukkuriVoice で使用している AquesTalk呼び出し機能を、ライブラリに切り出した。
たぶんMacで、Electronで、AquesTalkな読み上げアプリを作るなら役に立つでしょう。
めちゃくちゃレアケースだな!!!

※ MYukkuriVoice
www.nicovideo.jp


でも、ごめん使うのは大変

  • Macでしか動かないのであります
  • お試しするのにも、たくさん準備がいります
    • AquesTalkライブラリが必要
  • これを使って、アプリを作るなら更に準備がいります
    • AquesTalkライブラリの開発ライセンス、使用ライセンスが必要
  • たいへん(^_^;)


使い方

ライブラリの使い方の説明は https://github.com/taku-o/aquestalk-mac に書いてあります。

けれど、このaquestalk-macライブラリを使うにあたって大変なのは、
ライブラリの使い方より、導入までだと思うので、この記事では、その話をする。
おそらく、ライブラリそのものではなく、このサンプルアプリを動かすまでの手順が、このライブラリで一番価値があるものです。


サンプルアプリの動かし方

1. Pythonのバージョンを確認する

Macのデフォルトのsystemバージョンが望ましい。
他のPython環境を入れていてそれを使っていると、npm installで失敗することが多い。

2. npm installで必要なライブラリをインストール

git clone https://github.com/taku-o/aquestalk-player-mac.git
cd aquestalk-player-mac
npm install

3. electron-rebuildでnative系のライブラリを作り直す

electron-rebuildしないと動かない。

./node_modules/.bin/electron-rebuild

4. vendorEvaディレクトリに評価版ライブラリを入れる

  • AquesTalk2 Mac SDK評価版
  • AqKanji2Koe Mac SDK
    • この2つを入手して、"vendorEva"ディレクトリに次のようにファイルを配置する
|-- vendorEva
    |-- AqKanji2Koe.framework
    |-- AquesTalk2Eva.framework
    |-- aq_dic_large/aq_user.dic
    |-- aq_dic_large/aqdic.bin
    `-- phont/aq_f1c.phont

5. サンプルアプリを動かす

これでElectronのサンプルアプリが起動すると思います。
サンプルアプリでは入力した文字列を、AquesTalkを使って音声として再生したり、wavファイルとして保存したりできます。
お疲れ様でした。

cd sample
npm install
npm run start


このライブラリはファイルアクセスが必要?

Q. このライブラリを使用するにはファイルアクセスが必要?
    A. 必要

Q. つまり、Webブラウザでは動作しない?
    A. 基本的には動作しない

Q. 音声をJavaScriptで再生するには、Web Audio APIか、HTML5 Audioクラスあたりが妥当?
    A. 他のプログラムに作成した音声データを渡さないなら妥当

Q. Web Audio APIは、Webブラウザで動作しますよね?
    A. ・・・そうですね

Q. このライブラリはどういう場面で使えるの?
    A. ・・・え、Electronとか・






おまけ

ニコニコのランキングシステム変わるし、
SoftalkとかCeVIOとか、
動画の音声作成ソフトのタグには"テキスピ"使おうぜ、って話が出ている。

www.nicovideo.jp

Electronのrenderer processで、consoleのログ出力をターミナルに吐かせる

さいしょに

Electronアプリの開発中はelectronコマンドで直接アプリを起動すること多いと思う。

electron .
  • この時、main processでconsole.logると、ターミナルにログが出力される。
  • でも、renderer processでconsole.logしたら、ブラウザ側にログが出力される。


見る場所が分散していると面倒ですね。
だから、一カ所にまとめちゃおう。
両方ともターミナルにログ出力して貰おう、という話。

consoleの差し替え

renderer process内で、consoleを差し替えれば良い。

console = require('electron').remote.require('console');

console.log('log from renderer process');

おわり

おわり。
本番アプリではconsoleのコードは取り除かれるだろうから、開発中の話。
global変数のconsoleを差し替えると、eslintの推奨設定だと怒られちゃうけどね!