【make it easy】「pdf-libとpdfmakeをefwに使ってみる」の問題点と解決方法

本記事は以下の記事の続きです。

https://qiita.com/changkejun/items/6a966c87575989ac21b8

efw環境に、pdf-libとpdfmakeの試験を２週間続き、いろいろダーティテストをやって、問題点を２箇所に集中しました。
# 問題１：Promiseはマルチスレッドに向いていない
説明ですが、すべてのPromiseにこのような問題があるかどうかわかりません。自分がテストしたのは、promisejs.orgの[promise-7.0.4.js](https://www.promisejs.org/polyfills/promise-7.0.4.js) とそれを参考して作った[nashorn-ext-for-es6](https://github.com/efwGrp/nashorn-ext-for-es6)のPromise実装です。
## 問題説明
Promiseには、非同期実行のためsetTimeoutを利用します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3476221/4870434c-5310-740f-c9d8-b4f16704ff2b.png)
[nashorn-ext-for-es6](https://github.com/efwGrp/nashorn-ext-for-es6)のsetTimeout実装は、java.util.Timerを利用します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3476221/ec3b275d-a950-ca74-d8cb-43f967cd9401.png)
その中にはjava.util.TaskQueueを利用されています。taskQueueのqueueは固定サイズ128のtimerTaskの配列です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3476221/73687461-1c40-7dd0-9b7b-d45345b77b59.png)
想像してください。マルチスレッドのWEBサーバにとって、128は如何に小さい数字でしょう。
ブラウザーの場合、この制限はいかがでしょう？以下のプログラムでテストしてみました。chrome、firefox、edgeは全部クリアできて問題なさそうです。現在のブラウザーはすごいですね。
```html
<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を代替するクラスを作れば制限問題を解決になるかと思います。この方向は別途試します。

## 解決方法１：同期方式
PDF作成のイベントjsを同時１つリクエストしか対応しないようにすれば、マルチスレッド未対応の問題を回避できます。ユーザ数が少ない場合、この方法は簡単で分かり易いし全然よいのではないかと思います。nashornのJava.synchronizedを利用して、関数を同期関数を宣言するやり方を利用できます。
## 解決方法２：Pool利用
ユーザ数が多い場合、同時接続のユーザは全部隊列待ちになって、最後の人は長く待たないといけないです。改善しようとする場合、Poolの仕組みを利用する[loadWithGlobalPool](https://github.com/efwGrp/efw4.X/blob/master/help/global.loadWithGlobalPool.md)関数をつくりました。プールに複数スクリプトコンテキストを用意します。リクエストが来るとき、暇になっているスクリプトコンテキストを使って対応します。暇のスクリプトコンテキストがなければ、しばらくまってまたリトライします。
## ソース
### test.jsp
ボタンを押す場合、一括で１０回送信するようにしています。
```jsp
<%@ 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のそれぞれの
```javascript
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です。
```javascript
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はバイト配列です。
```javascript
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 +"'");
	
}
```
## 効果
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3476221/f1f667b3-58a1-18e3-0fdb-e259e4a18941.png)

test1.jsは、順番ずつでそれぞれの枠が表示されます。全部表示されるまで７秒です。test2.jsの場合、初回１６秒です。次回は４秒です。プールの効果はよく見えます。

## 想定する利用場面
pdf-libは座標定義方式で面倒くさいですが、既存PDFファイルに穴埋め方式で利用すれば便利だと思います。例えば、領収書印刷など、予めワードとか綺麗なレイアウトを作成してPDFに出力して、それをテンプレートとして利用します。穴の座標を測量してpdf-libのプログラムすれば簡単に実用可能のものを作れます。

ブラウザーのままでpdf-libを利用する場合、数MBのフォントクライアントにダウンロードしなければなりません。また、クライアントjavascriptだったら基本的にソースが見えます。証券番号など重要な書類の場合、作成ソースを公開されると偽装・詐欺とか出てきて危ないですね。

### 注意点
pdf-libのカスタマイズフォント利用の標準方法は以下です。PDFDocument.embedFontにバイト配列からembedderを作成しています。だが、このembedderは自動的にゴミ回収されないみたいで、大抵１００回実行したら、メモリリークが発生します。原因はまだ特定できていません。類似するように、画像の取り扱いも要注意でしょう。
前述今回のサンプルの場合、test1とtest2ともに、embedderを一回作成してずっと利用するからメモリリークを回避しています。

```javascript
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 }));
```

# 問題２：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

https://github.com/efwGrp/qittaSamples