本記事は以下の記事の続きです。
efw環境に、pdf-libとpdfmakeの試験を2週間続き、いろいろダーティテストをやって、問題点を2箇所に集中しました。
問題1:Promiseはマルチスレッドに向いていない
説明ですが、すべてのPromiseにこのような問題があるかどうかわかりません。自分がテストしたのは、promisejs.orgのpromise-7.0.4.js とそれを参考して作ったnashorn-ext-for-es6のPromise実装です。
問題説明
Promiseには、非同期実行のためsetTimeoutを利用します。
nashorn-ext-for-es6のsetTimeout実装は、java.util.Timerを利用します。
その中にはjava.util.TaskQueueを利用されています。taskQueueのqueueは固定サイズ128のtimerTaskの配列です。
想像してください。マルチスレッドのWEBサーバにとって、128は如何に小さい数字でしょう。
ブラウザーの場合、この制限はいかがでしょう?以下のプログラムでテストしてみました。chrome、firefox、edgeは全部クリアできて問題なさそうです。現在のブラウザーはすごいですね。
<html>
<script>
for(var i=0;i<100000;i++){
setTimeout(function(){console.log("ssss")},10);;
}
</script>
</html>
また、今回の調査により、setTimeoutの裏側にはqueueがあって、同じ実行時刻順の場合、queueに入る順番は、実行順番になるとわかりました。この特徴はProimseの非同期実行にはとても重要です。つまり、setTimeoutのQueueの制限は厳しいですが、Promise実装にとって論理的に大丈夫です。もしかして、java.util.Timerとjava.util.TaskQueueを代替するクラスを作れば制限問題を解決になるかと思います。この方向は別途試します。
解決方法1:同期方式
PDF作成のイベントjsを同時1つリクエストしか対応しないようにすれば、マルチスレッド未対応の問題を回避できます。ユーザ数が少ない場合、この方法は簡単で分かり易いし全然よいのではないかと思います。nashornのJava.synchronizedを利用して、関数を同期関数を宣言するやり方を利用できます。
解決方法2:Pool利用
ユーザ数が多い場合、同時接続のユーザは全部隊列待ちになって、最後の人は長く待たないといけないです。改善しようとする場合、Poolの仕組みを利用するloadWithGlobalPool関数をつくりました。プールに複数スクリプトコンテキストを用意します。リクエストが来るとき、暇になっているスクリプトコンテキストを使って対応します。暇のスクリプトコンテキストがなければ、しばらくまってまたリトライします。
ソース
test.jsp
ボタンを押す場合、一括で10回送信するようにしています。
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="efw" uri="efw" %>
<!DOCTYPE HTML>
<HTML>
<HEAD>
<META HTTP-EQUIV="CONTENT-TYPE"CONTENT="TEXT/HTML;CHARSET=UTF-8">
<TITLE>efw Output Test</TITLE>
<!-- Efwクライアントの取り込み-->
<efw:Client lang="jp"/>
<script>
function test1(){
$("#pdf0")[0].src="";
$("#pdf1")[0].src="";
$("#pdf2")[0].src="";
$("#pdf3")[0].src="";
$("#pdf4")[0].src="";
$("#pdf5")[0].src="";
$("#pdf6")[0].src="";
$("#pdf7")[0].src="";
$("#pdf8")[0].src="";
$("#pdf9")[0].src="";
Efw('test1',{pdf:'#pdf0'});
Efw('test1',{pdf:'#pdf1'});
Efw('test1',{pdf:'#pdf2'});
Efw('test1',{pdf:'#pdf3'});
Efw('test1',{pdf:'#pdf4'});
Efw('test1',{pdf:'#pdf5'});
Efw('test1',{pdf:'#pdf6'});
Efw('test1',{pdf:'#pdf7'});
Efw('test1',{pdf:'#pdf8'});
Efw('test1',{pdf:'#pdf9'});
}
function test2(){
$("#pdf0")[0].src="";
$("#pdf1")[0].src="";
$("#pdf2")[0].src="";
$("#pdf3")[0].src="";
$("#pdf4")[0].src="";
$("#pdf5")[0].src="";
$("#pdf6")[0].src="";
$("#pdf7")[0].src="";
$("#pdf8")[0].src="";
$("#pdf9")[0].src="";
Efw('test2',{pdf:'#pdf0'});
Efw('test2',{pdf:'#pdf1'});
Efw('test2',{pdf:'#pdf2'});
Efw('test2',{pdf:'#pdf3'});
Efw('test2',{pdf:'#pdf4'});
Efw('test2',{pdf:'#pdf5'});
Efw('test2',{pdf:'#pdf6'});
Efw('test2',{pdf:'#pdf7'});
Efw('test2',{pdf:'#pdf8'});
Efw('test2',{pdf:'#pdf9'});
}
</script>
</HEAD>
<BODY>
<button onclick="test1();">テスト1</button>
<button onclick="test2();">テスト2</button><br>
<iframe id="pdf0" src="" style="width:400px;height:300px"></iframe>
<iframe id="pdf1" src="" style="width:400px;height:300px"></iframe>
<iframe id="pdf2" src="" style="width:400px;height:300px"></iframe>
<iframe id="pdf3" src="" style="width:400px;height:300px"></iframe>
<iframe id="pdf4" src="" style="width:400px;height:300px"></iframe>
<iframe id="pdf5" src="" style="width:400px;height:300px"></iframe>
<iframe id="pdf6" src="" style="width:400px;height:300px"></iframe>
<iframe id="pdf7" src="" style="width:400px;height:300px"></iframe>
<iframe id="pdf8" src="" style="width:400px;height:300px"></iframe>
<iframe id="pdf9" src="" style="width:400px;height:300px"></iframe>
</BODY>
</HTML>
global.js
フォントのembedderを作成しておいて、同期のtest1.jsにそれを利用します。test2.jsにはpoolを利用するから、別スクリプトコンテキストのオブジェクトを利用できません。それで、フォントのバイト配列を予め用意して、poolのそれぞれの
var global={};
var bytesIPAexgothic=null;
var embedderIPAexgothic=null;
global.fire=function(){
load(_eventfolder+"/pdf-lib.min.js");
load(_eventfolder+"/fontkit.umd.min.js");
embedderIPAexgothic=await(PDFLib.CustomFontSubsetEmbedder.for(
fontkit,
byteToUint8Array(bytesIPAexgothic=file.readAllBytes("font/ipaexg00401.ttf")),
"IPAexゴシック",
{}
)
);
efw.register("PDFLib");
efw.register("fontkit");
efw.register("bytesIPAexgothic");
efw.register("embedderIPAexgothic");
function byteToUint8Array(byteArray) {
var uint8Array = new Uint8Array(byteArray.length);
uint8Array.set(Java.from(byteArray));
return uint8Array;
}
}
test1.js
Java.synchronizedのロックする対象はtest1に設定しています。任意のグローバル領域のオブジェクトで大丈夫です。例えば、ObjectなどでもOKです。
var test1={};
test1.paramsFormat={
pdf:null
};
test1.fire=Java.synchronized(function(params){
var pdfDataUri;
var pdfDoc=await(PDFLib.PDFDocument.create());
pdfDoc.registerFontkit(fontkit);
var myfont=PDFLib.PDFFont.of(pdfDoc.context.nextRef(), pdfDoc, embedderIPAexgothic);
pdfDoc.fonts.push(myfont);
var page = pdfDoc.addPage([500, 400]);
page.setFont(myfont);
page.moveTo(10, 200);
page.drawText('Hello World!こんにちは東京');
pdfDataUri=await(pdfDoc.saveAsBase64({ dataUri: true }));
return new Result().eval("$('"+params.pdf+"')[0].src='" +pdfDataUri +"'");
},test1);
test2.js
ES2015のテンプレートの書き方を利用しました。前述のようにpool外のものをpool内のスクリプトコンテキストに渡す場合制限があります。単純なものは大丈夫です。プロトタイプありのものはだめです。渡したいオブジェクトのプロトタイプはpool外で定義されて、pool内のスクリプトコンテキストに同名のプロトタイプがあるが、別物ですから、認識されないです。test2.jsにパラメータとして、渡すbytesIPAexgothicはバイト配列です。
var test2={};
test2.paramsFormat={
pdf:null
};
test2.fire=function(params){
var initscript=`
load(_eventfolder+"/pdf-lib.min.js");
load(_eventfolder+"/fontkit.umd.min.js");
var embedderIPAexgothic=await(PDFLib.CustomFontSubsetEmbedder.for(
fontkit,
byteToUint8Array(bytesIPAexgothic),
"IPAexゴシック",
{}
)
);
function byteToUint8Array(byteArray) {
var uint8Array = new Uint8Array(byteArray.length);
uint8Array.set(Java.from(byteArray));
return uint8Array;
}
`;
var runscript=`
var pdfDataUri;
var pdfDoc=await(PDFLib.PDFDocument.create());
var myfont=PDFLib.PDFFont.of(pdfDoc.context.nextRef(), pdfDoc, embedderIPAexgothic);
pdfDoc.fonts.push(myfont);
var page = pdfDoc.addPage([500, 400]);
page.setFont(myfont);
page.moveTo(10, 200);
page.drawText('Hello World!こんにちは東京');
pdfDataUri=await(pdfDoc.saveAsBase64({ dataUri: true }));
pdfDataUri;
`;
var pdfDataUri=loadWithGlobalPool({
name:"pdf-lib",
max:10,
initializer:initscript,
script:runscript,
context:{bytesIPAexgothic:bytesIPAexgothic}
});
return new Result().eval("$('"+params.pdf+"')[0].src='" +pdfDataUri +"'");
}
効果
test1.jsは、順番ずつでそれぞれの枠が表示されます。全部表示されるまで7秒です。test2.jsの場合、初回16秒です。次回は4秒です。プールの効果はよく見えます。
想定する利用場面
pdf-libは座標定義方式で面倒くさいですが、既存PDFファイルに穴埋め方式で利用すれば便利だと思います。例えば、領収書印刷など、予めワードとか綺麗なレイアウトを作成してPDFに出力して、それをテンプレートとして利用します。穴の座標を測量してpdf-libのプログラムすれば簡単に実用可能のものを作れます。
ブラウザーのままでpdf-libを利用する場合、数MBのフォントクライアントにダウンロードしなければなりません。また、クライアントjavascriptだったら基本的にソースが見えます。証券番号など重要な書類の場合、作成ソースを公開されると偽装・詐欺とか出てきて危ないですね。
注意点
pdf-libのカスタマイズフォント利用の標準方法は以下です。PDFDocument.embedFontにバイト配列からembedderを作成しています。だが、このembedderは自動的にゴミ回収されないみたいで、大抵100回実行したら、メモリリークが発生します。原因はまだ特定できていません。類似するように、画像の取り扱いも要注意でしょう。
前述今回のサンプルの場合、test1とtest2ともに、embedderを一回作成してずっと利用するからメモリリークを回避しています。
var pdfDoc=await(PDFLib.PDFDocument.create());
pdfDoc.registerFontkit(fontkit);
var myfont = await(pdfDoc.embedFont(fontBytes["IPAexゴシック"],{ subset: true }));
var page = pdfDoc.addPage([500, 400]);
page.setFont(myfont);
page.moveTo(110, 200);
page.drawText('Hello World!こんにちは東京');
var pdfDataUri=await(pdfDoc.saveAsBase64({ dataUri: true }));
問題2:pdfmakeのメモリ食い過ぎ現象
問題説明
上記pdf-libのサンプルのようにpdfmakeに対して類似サンプルを作りました。だが自分のマシンでpoolサイズを3以上設定すると、初期化失敗が発生します。pdf-lib.min.jsは513KBです。pdfmake.min.js+vfs_fonts.min.jsは2.05MBです。pool方式の場合、スクリプトコンテキストごとにロードするjsライブラリは同じJVMの中に共存するから、静的領域不足になる恐れでしょうかと思います。
解決方法
なし。
と言いたいですが、実はあります。JVMの外のものを利用すればいけるでしょう。例えば、v8とQuickjsです。chromeはv8を利用するから、chromeを10枚のタブで同じpdfmakeのサンプルを開いてpdfを作成することはできるでしょう。
後日また試験再開します。
今回のサンプルは、以下のフォルダにおいています。
hello-pdf-lib2とhello-pdfmake2