Skip to content

Instantly share code, notes, and snippets.

@chitacan
Last active August 22, 2020 07:15
Show Gist options
  • Save chitacan/71a8fdeb26568f487aa5 to your computer and use it in GitHub Desktop.
Save chitacan/71a8fdeb26568f487aa5 to your computer and use it in GitHub Desktop.
node.js vm

VM module

  • inside vm
  • vm.js
  • 성능??
  • 데모

나름의 계획

  • V8 Java Script Engine

  • vm의 테스트 코드

  • 다른 모듈(repl, tty) 과의 관련??

  • eval의 치명적인 단점은 코드 최적화가 이루어지지 않는다는 점이다.

vm의 용도??

  • 자바 스크립트 코드조각, 파일을 런타임에 실행
  • $ node filename.jsfilename.js 의 내용을 로드해 실행할 때
    • week_2.mdmodule load 과정 분석 참고

inside vm api

vm.js는 module.js 와 같이 evals 라는 네이티브 모듈을 통해 스크립트를 실행한다.

var Script = process.binding('evals').NodeScript;
var runInThisContext = Script.runInThisContext;

sandbox, context??

context

  • 하나의 v8 인스턴스에서 독립적인 자바스크립트 실행하기 위한 실행 환경
  • v8 에서 자바스크립트를 실행하기 위해서는 반드시 필요하다.

sandbox

  • v8은 보안상의 이슈로 context 생성시 만들어지는 global 객체를 바로 컨트롤 할 수 없다.
  • 대신 vm 의 메소드로 전달되는 sandbox 안지를 통해 간접적으로 global 처럼 사용할 수 있다.
  • sandbox의 프로퍼티 실행하려는 스크립트와 바로 공유되지 않고, context로 복사되었다가 스크립트가 끝나면 다시 원래 객체로 복사된다.

eval??

문자열을 자바스크립트로 해석하고, 이를 평가한 결과값으로 출력하는 자바스크립트 전역함수.

Davide Flanagan. 《 JavaScript The Definitive Guide 6/E 》. Insight. 105쪽. ISBN 978-89-6626-068-3

The Binding Method

gdb로 디버깅

$ gdb -d ~/Documents/workspace/node_project/node/src/ --args node vm_study.js
(gdb) b node.cc:Binding
(gdb) run
(gdb) p *modp
$17 = { 
  version = 1,
  dso_handle = 0x0, 
  filename = 0x10039a2f4 "../src/node_script.cc", 
  register_func = 0x100015ffb <node::InitEvals(v8::Handle<v8::Object>)>,
  modname = 0x10039a30a "node_evals"
}
(gdb) b node_script.cc:443

참고로 node.cc:Binding 은 node 초기 시작과정에 evals 외에도 natives, buffer, fs, constants, tty_wrap, timer_wrap, cares_wrap, signal_watcher, crypto 모듈을 바인딩 한다.

  • 자바스크립트 객체를 생성하고 생성 정보를 캐시

binding evals

process.binding('evals') 이 호출되면,

  • Binding이 호출되고,
  • get_builtin_modulenode_module_list 에서 eval과 관련된 구조체를 리턴해 준다. 여기에 eval 모듈과 관련된 초기화 함수가 지정되어 있다. (위의 modp 참고)
    • 이후 해당 구조체에서 register_function을 호출하고, 캐쉬에 저장
    • exports 객체를 넘겨주는데 이거 뭐임?
    • node_extensions.cc:node_module_list 는 누가 세팅해줌???
  • register_function에 지정된 InitEvals 가 호출되면,
  • WrappedScript::Initialize 를 통해 NodeScript 심볼을 생성한다.(오브젝트를 만든다?)

결국 evals 를 바인딩하고 NodeScript 인스턴스를 통해 메소드를 호출하면, node_script.cc의 메소드들이 호출된다.

process.bindingaddon에 사용되는 process.dlopen 과는 다르게 이미 초기화 함수의 위치를 알고 시작한다.

EvalMachine ??

vm.js의 메소드들(createContext, runInContext, runInThisContext, runInNewContext)은 결국 node_script.cc 의 WrappedScript 클래스에 존재하는 동명의 메소드들과 연결된다.

이 메소드들을 살펴보면 모두 WrappedScript::EvalMachine 메소드를 호출한다.

gdb로 살펴보면

$ gdb -d ~/Documents/workspace/node_project/node/src/ --args node vm_study.js
(gdb) b node_script.cc:317
(gdb) run

EvalMachine 함수의 큰 흐름은 다음과 같다. (runInNewContext 경우)

  • args 에서 코드, 파일이름 추출
  • context 생성 하고
  • 전달받은 sandbox의 프로퍼티를 context에 복사한다.CloneObject
  • V8엔진의 Script 인스턴스를 만든뒤
  • Script->run() 을 통해 스크립트 실행
  • 실행이 성공하면, context의 프로퍼티을 sandbox에 복사한다.

ControlFlowGraph-EvalMachine.svg 이미지 참고

즉, sandbox 객체의 내용은 공유되지 않고 복사된다.

이 흐름은 여기 에 좀 더 간단하게 설명되어 있다.

Context::New()

여기 에 선언되어 있음

CloneObject

https://github.com/joyent/node/blob/v0.8.18-release/src/node_script.cc#L104

  • 자바스크립트 문자열(siaf)을 만들고 컴파일 한뒤,
  • 해당 자바 스크립트 문자열을 실행한다.
    • 자바스크립트 문자열 내부에서는 source의 프로퍼티를 target에 정의한다.

왜 이놈은 자바스크립트 코드로 넣어놨지?? ㄷ

Script 클래스

결국 스크립트 코드를 받아 실행을 하는 클래스는 Script 클래스 이다.

Script:Compile 메소드의 구현은 api.cc 에서 찾을 수 있다.

vm.js의 Script 함수

Script 함수의 역할은 새로 읽는 파일의 함수 호출시 this 키워드가 ns를 가리키도록 하는 것이다.

return ns[f].apply(ns, arguments);

createScript에서 ctx를 어떻게 생략하고 호출할 수 있지?

how to use api

vm.runInThisContext(code, [filename])

  • 전달된 code를 컴파일하고 실행한다. 실행시 로컬 스코프에 엑세스 할 수 없다.
  • 외부와 데이터 교환이 없는 단독코드를 실행할 때. (가장 빠르다.)

vm.runInNewContext(code, [sandbox], [filename])

  • 새로운 context 를 만들고, sandboxglobalcode를 실행한다.
  • sandboxcode가 실행되기 전에 context에 복사되었다가, 실행이 끝나면 다시 로컬 스코프의 sandbox 로 복사된다.
  • code 실행시 로컬 스코프의 내용을 반영하고 싶거나, code의 수행결과를 로컬 스코프로 가져오고 싶은 경우
  • sandbox 의 프로퍼티가 많으면 많을수록 느려진다.

vm.runInContext

  • 이미 만들어진 context를 통해 code를 실행한다.
  • vm.createContext와 묶여서 실행됨.
  • 동일한 context를 여러번 재활용 하고자 할 때.

vm.createScript(code)

  • 전달된 code를 컴파일만 한다. 코드는 나중에 실행시킬 수 있다.

script.runInThisContext()

  • script를 현재 컨텍스트에서 실행한다. 실행시 로컬 스코프에 엑세스 할 수 없다.

script.runInNewContext([sandbox])

  • sandboxglobal로 코드를 실행한다.

node의 global 인스턴스를 context로 전달하기

아래의 코드를 통해 node의 global 인스턴스를 포함한 context를 만들고 코드를 실행할 수 있다.

var target = {};

Object.getOwnPropertyNames(global).forEach(function(key) {
  var desc = Object.getOwnPropertyDescriptor(global,key);
  Object.defineProperty(target, key, desc);
});
context = vm.createContext(target);
vm.runInContext("console.log('hello');", context);

vm의 성능은?

최적화는 적용되고 있을까?

C++ 내부코드에서 컴파일 함수가 호출되는 것으로 보아 v8 엔진의 코드 최적화가 이루어 지고 있는 듯한데,

Google I/O 2012 - Breaking the JavaScript Speed Limit with V8 에서 사용된 prime.js 로 테스트 해 보았음

25000 번째 소수를 찾는 코드. v8 이 최적화 할 수 있는 부분이 포함되어 있다.

최적화 되기 어려운 코드를 실행하면

$ time node prime_no_opt.js
287107

real    0m13.706s
user    0m13.604s
sys     0m0.090s

최적화 과정을 거친 코드를 실행하면

$ time node prime.js
287107

real    0m0.091s
user    0m0.074s
sys     0m0.015s

vm 을 통해서 실행해 보면, 응? 차이가 거의 없네?? v8의 성능은 제대로 발휘되고 있는 듯 하다.

$ time node prime_vm.js
287107

real    0m0.092s
user    0m0.075s
sys     0m0.016s

API간의 성능차?

bench.sh를 돌려보자, sandboxcontext는 아래와 같이 적용

var sandbox = global;
var context = vm.createContext(global);

결과는 다음과 같다.

vm - runInThisContext : 33ms
vm - runInNewContext  : 52ms
vm - runInContext     : 45ms
script - runInThisContext : 30ms
script - runInNewContext  : 46ms
  • runInThisContext 가 빠른 것은 당연한 듯하다. CloneObject 가 호출되지 않기 때문이다.
  • runInContextCloneObject 호출 횟수가 줄어 runInNewContext 보다 빠르다.
  • script.runInThisContext 의 경우 미리 컴파일된 스크립트를 사용하기에, 대부분의 경우 vm.runInThisContext 보다 (아주 근소한 차이로) 빠르다. 컴파일 시간이 오래 걸리는 코드라면 속도의 차이가 크게 날 듯 하다.

사용되고 있는 곳?

Runnable Editor

 ______________ 
< demo time !! >
 -------------- 
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

see demo app on heroku!!

hack point

신기하게도 bench_wierd.js의 마지막 bench 메소드의 호출 순서를 바꾸면 결과가 다르다. 왜일까?

  • gc??
  • 클래스의 재활용??

컨텍스트 자체를 serialize 하는 것도 가능할까??

see also

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment