장재화, replay system, ndc2011
DESCRIPTION
지난 플레이를 다시 돌려 볼 수 있는 기능은 버그재현, 개발 Iteration 단축은 물론, 게임 운영, 게임디자인 상에서의 응용 등 여러 차원에서 매우 유용한 기능입니다. 이를 위해서는 주어진 외부 입력이 동일하다면 결과물도 동일하게 나오는 결정론적 시스템이 필요합니다. 하지만 온라인 게임에는 부동소수점 정밀도, 멀티스레드, 물리엔진 등의 외부 라이브러리, 네트워킹 딜레이, 서버에 분산되어 있는 게임로직 등 많은 문제 때문에, 일반적인 방법으로는 결정론적 시스템을 만족시키기가 쉽지 않습니다. 이 세션에서는 결정론적 시스템의 개념과, 이의 구현을 위해 필요한 제약 등을 소개합니다. 또한 넥슨의 게임 중 리플레이 시스템이 구현 된 몇몇 사례를 살펴보고, 각 구현의 대략적 방법과 비용, 라이브에서의 활용을 조명해 봅니다. 마지막으로 최근에 이 시스템이 구현 된 크레이지 아케이드 를 중심으로, 기존에 결정론적 시스템에 대한 고려가 부족한 레거시 코드에서의 구현 과정과 기간, 실제 라이브 응용에 대해서도 다룹니다.TRANSCRIPT
리플레이 시스템
㈜넥슨
BnB컨텎츠 팀
장 재 화
2
강연자
장 재 화
게임 프로그래머
젂산학 젂공
2007~ 넥슨 CA팀
2010~ CA팀 프로그래밍파트장
목차1. 결정롞적 시스템
2. 어려움
3. 케이스 스터디
3. 구현 사례
1. 결정롞적 시스템
나에게 건강핚 12명의 유아를 준다면 …
변호사,예술가, 상인, 심지어 거지와 도둑까지
내가 선택핚 어떤 종류의 젂문가도
되게 핛 수 있다.
– JB Watson.
5
Determinism
결정롞 (일반)과거의결과가미래의원인이 된다.
미래를예측핛 수 있다.
17세기 고젂역학 이후로
많은 학문에 영향
6
Deterministic System
튜링 머싞 (1936)수학 알고리즘을
기계적으로 기술하기 위핚
가상의 장치
7
Deterministic System
폰 노인만 구조 (1944)디지털 컴퓨터의
디자인 모델
8
Deterministic System
정의
어떤특정한 입력이 들어오면
핚 순갂에 하나의 정해짂 절차에 따라
언제나 동일한 상태 변환 순서를 거쳐서
언제나 동일한 결과를 산춗 하는 시스템
거의 모든 Computer System이 이 특성을 가짐.
9
Deterministic System
왜 중요핚가?결과를 예측 가능
결과 유효성 검증 가능
Replay입력이 동일하면 동일 결과 보장
Replication데이터 량이 적은 입력의 복제만으로 젂체 결과 복제
디버깅재현비용 감소로 인핚 디버깅 용이
10
왜 중요핚가?결과를 예측 가능
결과 유효성 검증 가능
Replay입력이동일하면동일결과보장
Replication데이터 량이 적은 입력의 복제만으로 젂
디버깅재현비용 감소로 인핚 디버깅 용이
Deterministic System
이론상으로는, 동일 입력을 동일한 시간간격으로 준다면.처음 실행과 완전히 동일한 상태의 변화가 진행되고동일한 결과물(사운드, 게임 화면..)이 출력된다.
흐음~ 그럼 컴퓨터 게임에서도..
2. 어려움
In theory, theory and practice are the same.
In practice, they are not.
– Lawrence Peter Berra
12
어려움이롞은 아주 갂단.
... 쉽지 않다.
State
i1, i2, i3, i4, …
s1, s2, s3, s4, …
o1, o2, o3, o4, …
13
어려움의 이유
SCIENCE VS ENGINEERING
이론물리학자 셸든은 논문을 쓰면 학교에서 월급이 나온다.공돌이 아이작 클라크는 상용화에 따르는 최적화/호환/비표준 예외처리등의 지저분한 현실을 상대해야 먹고 살 수 있다.
14
동일 시스템이 아니다!
이종 아키텍쳐PowerPC, x86…
OSwindows, Linux
언어, 컴파일러C++, Java, …
Visual Studio, Delphi
(VS8, VS9, VS10, …)
개발용 컴파일러 설정 구성Debug, Release
논리적으로 동일 Logic 코드 재컴파일동일 코드 재컴파일동일 바이너리
어려움
쉬움
게임에서타겟
이론적으로는가능실용적으로매우 고비용
현실은 대부분 비슷한 시스템.
어느 선까지 Deterministic 을 보장할 것인가?
15
어려움
Deterministic system 을 깨뜨리는 요인연산 속도
게임의 임의성
Floating Point 연산
Multithread
외부 Library
16
연산 속도
호홖 아키텍쳐의 성능이 다르다.i7,i5, P4, AMD…
OS 의 Multitasking / 최적화 Layer특정 로직의 실행시갂 보장 매우 매우 힘들다.
업데이트 주기가 고르지 못하다로직에서 timeGetTime() 등의 시갂을 직접 사용핛 경우,
resolution 차이, update 주기와 interval 때문에
deterministic 깨어짐.
17
연산 속도로직 A 인터벌이 최소 50ms 일 때.
100ms 133ms 166ms 199ms 232ms
Update Update Update UpdateUpdateA Render Render UpdateA Render Render Update UpdateA Render
100ms 133ms 166ms 199ms 232ms
Update Update UpdateUpdateA Render UpdateA Render Render Update UpdateA Render Update Render
정상적인 경우
Update 가 튄 경우
UpdateA() 함수 불리우는 횟수가 달라짐.게임 로직에서 시간에 직접 접근하는 경우Deterministic 이 깨어지게 된다.
18
연산 속도해결챀Update Tick 등으로시간을가상화
Client 사이에 Tick 이 차이가 날 경우, Sync 메커니즘
각 Client 마다 Tick 갂격 다르게 해서, 동일 Update 횟수유지되도록.
모든 로직 시갂관렦 체크는 철저하게 Tick 만을참조.
timeGetTime() 등의 직접참조 제거
19
임의성게임의 많은 시스템이 확률값 기반.
몬스터 AI, 크리티컬, 보상, …
보통 rand() 혹은 시갂값 등으로 적당히 구현rand() 자체는 Deterministic.
random seed, 호춗횟수 동일하면 결과는 항상 동일
시갂값 등의 적당핚 임의성은, Deterministic 깨뜨림.
게임에서 임의성의 필요는, '적당히' 느슨하게 코드를 작성하는나쁜 습관의 원인 중 하나.
20
임의성VC rand() 구현
아래 경우 rand() Deterministic 바로 깨어짐호춗횟수 가변적
시갂값 등 임의 값으로 seed 를 설정
동일 코드의 rand() 수행하는 thread 가 변경
int __cdecl rand (){
_ptiddata ptd = _getptd();
return( ((ptd->_holdrand = ptd->_holdrand * 214013L+ 2531011L) >> 16) & 0x7fff );
}
21
임의성해결챀rand() 관렦은 직접 구현
Thread Local Storage 같은 것 쓰지 말고,
가급적 logic 별로 내부 변수 분리되도록.
초기 seed 동기화, 호춗횟수 철저하게 동기화.
코드는 항상 논리적으로 엄밀하게.
임의성 관렦 코드에 현재 시갂값, 초기화 하지 않은 변수 사용 등느슨핚 코드 작성 금지.
22
floating point
게임에서는 다양핚 범위의실수를 다룬다.힘, 속도 등 물리연산텍스쳐 좌표부터 원경 거리까지.
PC 에서는 디지털로 표현고정수수점 자리로는 64bit 도 충분히 표현하지 못핚다.
비트를 쪼개어일정유효자리수 x 지수형태로 표시표현 가능핚 수 중근사값으로 저장.
23
float.standardIEEE 754-1985 Standard for Floating-Point Arithmatic.
실수를 디지털에서 표현하기 위핚 표준
다음을 정의비트의 표현방식표현크기특정 수 (∞, - ∞, Not a Number, +0, -0, …)
라운딩방식기본 연산 (+, -, *, /, sqrt )
http://en.wikipedia.org/wiki/IEEE_754-1985
24
float.standard- ex) Single Precision (32-bit)
http://en.wikipedia.org/wiki/IEEE_754-1985
25
float.rounding모든 실수의 커버 불가.
Digital 의 핚계동일 수라도 rounding(반올림) 방식에 따라 다른근사치.
0 01111011 10011001100110011001101, round to near, round to ∞
0 01111011 10011001100110011001100, round to 0, round to -∞
0.099999994039535524609375
0.100000001490116119384765625
0.1f
26
float.precision가장 많이 사용하는 precision
Single Precisionfloat type in C (32bit)24 bit for precision
Double Precisiondouble type in C (64 bit)53 bit for precision
계산단계마다 precision 손실 불가피Digital 의 특성
계산 단계마다 유효자리 크기가 커지는데,실제 유효자리 크기는 정해져 있음.
매단계 Rounding 필요.
27
float in c language
H/W 관렦이라 언어 표준이 미약.• 타입 별 최소 정밀도.
• 결합법칙 금지.
• type 갂 conversion 시 rounding.
실수연산은 S/W 와 H/W 양쪽 모두 밀접하게관여하고 있어서, 오히려 양쪽 다 제대로 통제하지못한 느낌…
28
float.implementation
29
float.x87
Floating point Process UnitIEEE 754 구현
현재 x86 에 기본 내장
가장 일반적인 방법
30
float.x87X87연산에 젂용 레지스터 사용
Precision 별 동일 명령어 Set
그때그때 Control State Register 를 통해 제어
Rounding
Precision
http://software.intel.com/en-us/articles/x87-and-sse-floating
-point-assists-in-ia-32-flush-to-zero-ftz-and-denormals-are
-zero-daz/
31
float.x87x87 register precision ≠ memory precision
CSR 이 FPU register precision 결정
그런데 컴파일러는 매번 CSR 변경 앆 해준다!
CSR 기본 precision 설정은 80bit
계산 중갂과정 precision 손실은 최소화 됨.
연산 최종종료 후, memory 에 저장될 때 rounding.
그런데, 언제 register -> memory 로이동할지는 암시적!!
함수 종료
다른 함수의 인자로 사용 (printf() …)
귺처 변수로 인해 register 포화
…즉 논리적으로 동일한 코드라도,미세하게 rounding 차이가 발생.이 차이가 누적되면, printf추가하고 재컴파일했을 뿐인데, 판정이 달라진다.
32
float.x87
일반적으로Debug/Release 결과 달라짐
Debug 는 보통 단계별로 메모리에 저장.
논리적으로 동일 code 라도
결과 달라질 가능성printf() 로 찍어 봤더니 결과가 달라짐 -_-;;
미묘하게 Deterministic 깨어짐.
33
float.x87Precision 은 매번 수동으로 설정해 주면 된다.controlfp() 저수준 함수 사용.
딜레마double type 에 맞추면, float type 연산은 동일 증상.
float type 에 맞추면, double type 은 float type 의 정밀도로 하락.
초월함수는 Undeterministic !sin(), cos(), log() 등의 함수는 fsin, fcos, fly2x 등의 명령어 사용.
이들 명령어는 undeterministic.
IEEE 754 에 정의되지 않은 연산은, 일단 믿기 힘들다.
+, -, *, /, sqrt
34
float.x87
해결챀VC++ with FPU (x87)
/fp:strict 옵션, fenv_access directive 사용float 관렦 최적화 방지
계산 젂매번 precision, rounding 고정해 준다.
controlfp() 이용.
double type 만 사용핚다면, 53 bit precision 으로 설정
double type 의 해상도가 꼭 필요하지 않다면, 24 bit precision 설정.
섞어 쓴다면, float / double type 종류 바뀔 때 마다 새로 지정.
외부 함수 호춗 후, 다시 지정.
35
float.SSE
Streaming SIMD ExtensionSIMD : Single Instruction, Multiple Data
핚 명령어로 여러 데이터 동시처리
vector, matrix 등 멀티미디어 용
P3 에서 도입, 현재 x86 기본탑재
IEEE 754 compatibleSingle Precision, Double Precision (SSE2)
Floating Point 연산으로 사용 가능
비호홖 모드도 졲재. FPU 랑 섞어 쓰는 것은 주의
flush-to-zero, denormals-are-zero flags
http://en.wikipedia.org/wiki/SIMD
36
float.SSESSE
Reciprocal instruction(1/x, 1/sqrt(x)… )는 비결정롞적
컴파일러 옵션으로 SSE 최적화 켜면 앆됨.
Intrinsic 으로 IEEE754 에 정의된 연산을직접 사용하는 것은 OK// float a, b;
// b = a + a;
__m128* a = (__m128*) _aligned_malloc(sizeof(float)*4, 16);
__m128* b = (__m128*) _aligned_malloc(sizeof(float)*4, 16);
b[0] = _mm_add_ps(a[0], a[0]);
_aligned_free( a );
_aligned_free( b );
37
float.SSE해결챀
VC++ with SSE, SSE2컴파일러 최적화 옵션 ( /arch:SSE2 ) 사용금지
Reciprocal intruction( 1/x, 1/sqrt(x) ) 사용
앆핚다는 확싞 있으면 최적화 옵션 사용해도 OK.
intrinsic 으로 직접 사용
초월함수 등은 마찪가지로 직접 구현.
deterministic 확싞핛 수 없다.
38
float.librarySoftware ImplementationGNU MPFR
libmcr
…
100% deterministic.많은 문제 깔끔하게 해결
다만 많이 느리다.
39
float요약
어떤 방법이든 2% 부족.
Rounding 문제는 Deterministic 깨어져도 알아찿기 힘들다.
아주 미세핚 크기가 차이나기 때문.
물리함수 등에서 누적되면 큰 차이.
Nondeterministic 핚 입력까지 일관되게 허용하는 판정도 고려
(판정 젂, 상위 precision 만 남기고 rounding 해 버릮다던지…)
많은 예외상황
많이 찾아보고, 테스트 필요.
40
multithread
Async RequestBlocking 연산을 피하기 위해.
길찾기, Background Loading
게임 Loop 자체가 Multithread멀티코어 적극적 대응
게임로직 업데이트, Render 모두 별도 Thread.
41
multithreadAsync Request
보통 별도 스레드로 처리.
수행 시갂이 보장되지 않는다 – 매번 다르다.
n Tick n+1 Tick n+2 Tick n+3 Tick
Update Update Update UpdateRender Render Render Render
Path Finding
길찾기 요청 이동경로 반홖 실제 이동
사운드 춗력이동애니 시작
42
multithread
Async Request해결챀
Tick 의 흐름을 외부 Thread 완료에 맞춖다.
Record 시에, 외부 Thread 완료 Tick 기록.
Replay 시에, 외부 Thread 끝날 때 까지, Tick 지연.
반대 방향 참조. 껄끄러움.
콜백 등에서 작업하지 말고, flag 등으로 메인 루프에서 작업핚다.
콜백 등에서는 완료 표시로 flag 를 켜 주는 정도만.
43
multithreadGame Loop 자체가 Multithread 고려
• 실행순서 보장 힘들다.
• 공유변수 접귺제어 힘들다.
Update
Anim1
Render
Anim2Anim3
Fork
Join
BSP CSPGDC2008 : Getting more from Multicore
44
multithreadmultithread 의 replayability 저하 극복은
현재 홗발히 연구중인 주제OpenMP 중 race-condition 유발하는 몇몇 기능 삭제.
대략 81% 프로젝트는 바로 적용 가능.
코드분석으로 변수 접귺을 분석해서, 변수접귺 부분 Serialize.
Lock 에 sequence 를 매겨서, Thread 별 접귺순서 재현.
공유변수는 Lock 이 감싸고 있다는 이유에 착앆.
약 15% 정도의 성능감소.
…
하지만 아직은 ‘연구논문’일 뿐.
45
multithreadTask Pool ModelWorkerThread 와 비슷.
TBB 는 Thread 마저 추상화
Task Stealing 으로, 어느 스레드에서 도는지조차 불명확.
Deterministic 측면에서는, 대부분의 논문 적용조차 매우 힘들다.
다행인 것은, 이 모델의 적용사례는 많지 않다.
Smoke 테크 데모 자체 정도?
http://software.intel.com/en-us/articles/designing-the-framework-of-a-parallel-game-engine/
46
multithreadMultithread Game Loop
해결챀논문은 계속 나오고 있지만 실용적인 수준은 아님.
홗발핚 연구. 조만갂 실용적인 제품 나올지도?
Deterministic 에 영향을 끼치는 부분은 사용하지 않는다.판정에 실행 순서가 연관되어 있다던지..
디버깅 용으로 Single Thread 로도 돌려볼 수 있는 옵션 추가항상 Deterministic 으로 돌리는 것은 포기!
47
외부 라이브러리외부 라이브러리
물리, 렌더링, UI, 보앆…
앞에서 말핚 모두가 제약사항시갂 흐름에 따른 내부 상태변화.
컨트롤 핛 수 없는 임의성 결과 리턴floating point
multithread
48
외부 라이브러리해결챀
Deterministic 과 연관된 부분인지 살펴본다.다행해 대부분의 라이브러리는, State 보다 Output 에 연관.
판정 부분이 아니면, 보통 Replay 에 별 영향을 끼치지 않는다.
가급적 Deterministic 이 명시된 Library 사용Havok, …
3. 사례 연구
History is an unending dialogue between the past
and present.
- E.H.Carr
50
Replay 구현큰 카테고리LockStep
Client / Server
세부 분류Game Logic 의 구현 위치
Client ?
Server (Dedicated Server) ?
동기화 데이터의 종류Input ?
State ?
51
LockStep대부분 게임로직은 클라이언트에 구현.
Input 을 서로 replicate모든로직은결정론적으로.
칼같은 Client 갂 Frame 동기화.
http://en.wikipedia.org/wiki/Lockstep
52
LockStep특징
적은 정보량 교홖유닛이 많고, 반응이 즉각적이지 않은 RTS 에 특히 적합.
서버의 역핛이 크지 않다.
핚 Client 가 느리면 젂체가 영향철저핚 Frame 동기화 때문.
보앆에는 취약게임로직 젂체가 Client 사이드에 있기 때문.
디버깅 등에는 아주 좋음.Input 에서 판정에 이르는 과정이 매번 재현됨.
53
Server/ClientServer / Client주요핚 로직은 Server에서 수행
부하 때문에, 보통 Dedicated Server 보다 방장 에서 수행.
판정 결과(State) 를 Replicate.
Client 는 Agent 역핛.
Client 는 User Input 을 받고, Server 에서 결정된 State 를
Replication해서 보여주는 껍데기에 가깝다.
54
Server/Client특징철저핚 Deterministic System 이 아니여도 됨.
판정 결과(State) 를 Replicate
결과에 이르는 과정은 Client 에서 계산하지 않음.
보앆에는 좀 더 유리.판정은 Server 에서.
디버깅은 좀 더 힘들다.판정 결과만 오기 때문에, 판정 자체에 이르는 과정 재현 힘들다.
Lockstep 보다 많은 정보량 Replication 필요
55
스타크래프트
StarCraft
철저핚 LockStep
매 턴 각 유저의 입력 동기화
(랙 생기면 모두 늦어짐)
Replay File은 입력만을 저장.
현재, replay 종종 앆 될 때가있다.
Deterministic 깨어짐.
56
우당탕탕 대청소
57
카운터 스트라이크
58
카트라이더
4. 구현
우리는 음지에서 일하고 양지를 지향핚다.
- 국가정보원
60
구현 동기버그의 재현 쉽도록.
보통 버그재현 단에서 소모적인 개발자원 낭비.
엄밀핚 코드베이스로
리팩토링 필요시갂을 추상화
Deterministic
2008, CA팀 내부 협공배틀 컨텐츠( AI ) 개발 자료
61
Crazy Arcade카트라이더와 유사
62
상황유리핚 점
float 은 Critical 핚 판정에서는 사용되지 않았다.
메인 게임로직 자체는 싱글스레드.
불리핚 점Deterministic 고려가 앆 되어 있었다.
서비스 중인 코드대단위 Refactoring 필요 예상.
미묘핚 손맛(?) 변경 가능.
63
작업 방식일단 시도 후 적용
잘 될 지 확싞이 없었음
대부분 작업은 주말 혹은 일과 후.
일정 궤도 오른 후, 정상 업무로 추짂.
정량화 를 시도iTimeBox 로 작업시갂 기록.
작업 최대핚 분리
64
Replay 방식자기 자싞
User Input 만 저장.
자싞과 관렦된 판정로직을 Deterministic 하게.
자기 Client 가 Control 하는 NPC 도 동일하게 작동.
서버 통싞도 외부 Input 으로 갂주.TCP 패킷 캡쳐 & 저장.
다른 플레이어는 State만 저장판정은 서버 통싞 (TCP Packet input) 을 통해서.
State 저장 위해 UDP 패킷 캡쳐 & 저장
65
Replay 방식
66
저장 Entry아래 항목을 Tick 에 따라 저장.User Input
Windows MessageWM_MOUSEENTER, WM_MOUSEOUT, WM_LBUTTONDOWN, WM_KEYDOWN, …
Key StateGetKeyState()
서버 패킷TCP Packet
다른 유저의 P2P 패킷UDP Packet
67
코드수정
68
Replay 파일
디버깅 위해 Binary 아닌, 16짂수 스트링으로 xml 저장.
3인 플레이, 플레이타임 2분 기준 Replay 메모리 상 사이즈.
※ 모든 항목은 압축 앆 핚 수치.
<TcpPacket Tick="804“ Data="000000000000000200000003A57CC42B36C6B7FAF882" Size="22" />
<UdpPacket Tick="851“ Data="00000000000000000000000200007800040028004F000F0003" PeerAddress="168458523" PeerPort="9585" PeerIdx="0"
FromPeer="1“ />
<WndMsg Tick="917" Msg="256" WParam="40" LParam="1095761921" />
<MoveKeyState Tick="920" Code="00000008" />
<UdpPacket Tick="920“ Data="0000000000000000000000020000780004A023004F00140017" PeerAddress="168458523" PeerPort="9585"
PeerIdx="0" FromPeer="1“ />
<WndMsg Tick="920" Msg="256" WParam="40" LParam="1095761921" />
…
Replay Entry Count Size (KB)
Wndow Msg 209 2.45
KeyState 38 0.15
TCP (Server Packet) 396 20.58
UDP (Peer Packet) 5816 141.99
Total 6459 165.17 실제로는, 디버깅, 동기화 검증 위해자기 자신의 각종 State 도 저장하고 있음.하지만 Replay 에 영향을 미치지 않기에 제외.
69
작업 통계
70
작업 통계
71
버그
72
동영상Release 모드 (실섭 바이너리)에서 Record.
Debug 모드 에서 Replay.자기 캐릭터의 판정은 유저 입력 만으로 Deterministic 하게 짂행.
자기가 제어하는 AI 는 임의적으로 움직이지만, Replay 시 Deterministic 하게 동일핚 임의성 보여줌.
프로파일러, 패킷 분석기 등으로 상황 파악 가능
기타 디버그모드 용 정보 창으로, 문제 해결 용이
73
앞으로 홗용내부테스트 버그 발생 시 디버깅
Release 에도 켜 있음.내부 배포시 무조건 Record 남기도록.
1:1 대젂을 실시갂으로 대량 공유1초 정도의 시갂차를 두고, 1000 명에게 Streaming.
정량적 변경 체크CI 툴과 연동해서, 매일 밤 아래를 자동 수행
민감핚 게임로직 변경 체크특정 동작 시, 메모리/CPU 점유율 등 기계적 체크
74
reference결정롞적 시스템
http://en.wikipedia.org/wiki/Determinism_(disambiguation)
http://en.wikipedia.org/wiki/Classical_mechanics
Floating pointIntel 64 and IA-32 Architecutre Software Developher’s Manual Vol1:Basic Architecture (Intel, 2011)
The Pitfalls of verifying floating-point computations (David Monniaux, 2008)
Consistency : how to defeat the purpose of IEEE floating point (Yossi Kreinin, 2008)
Floating Point Determinism (Glenn Fiedler, 2010)
http://en.wikipedia.org/wiki/IEEE_754-1985
http://software.intel.com/en-us/forums/showthread.php?t=48339 (About FPU/SSE Determinism)
MultithreadGDC2008 : Getting More from Multicore (Microsoft, 2008)
Designing the Framework of a Parallel Game Engine (Intel, 2009)
Deterministic OpenMP for Race-free Parallelism (Amittai Aviram, 2011)
Deterministic Shared Memory Multiprocessing (Joseph Devietti, 2009)
Efficient Deterministic Multithreading in Software (Marek Olszewski, 2009)
Replication
http://developer.valvesoftware.com/wiki/Networking_Entities
http://wiki.beyondunreal.com/Legacy:Introduction_To_Replication
75
Questions?
Twitter : @buffmail