Thursday, May 18, 2006

Rail - load testing

คุณ Revolution ถามเกี่ยวกับเรื่อง performance มา
โดยพบปัญหาว่าทดสอบแค่ 100 concurrent connection ก็เจอ Error 500 ซะแล้ว

ผมก็เลยทดลองดูบ้าง
โดยกำหนดสถานะการณ์เป็น 2 แบบคือ
  • 100 request รุมเข้าไปภายใน 1 วินาที โดยมีการกระจายตัวแบบ Gaussian
  • เหมือนข้อแรก แต่ยิงติดต่อกัน 3 วินาที (300 request)

UPDATE - page ที่ใช้ทดสอบเป็น scaffold page ในส่วนของ list method
โดยดึง data จาก mysql โดยในเบื้องต้น ยังไม่สนเรื่อง ขนาดของ Database

เครื่องที่ใช้ทดสอบคือ
  • Laptop Compaq Presario 2545AP
    cpu pentium 4 2.6GHz, Ram 1 GB.
  • Apple G5, Dual Cpu 1.8 GHz, Ram 1.25 GB


config ของ Fcgi กำหนดให้ start ไว้ 7 processes
<IfModule mod_fastcgi.c>
FastCgiIpcDir /tmp/fcgi_ipc
FastCgiServer /Library/WebServer/Documents/perf/dispatch.fcgi
-idle-timeout 60 -initial-env RAILS_ENV=production -processes 7
</IfModule>


ผลการทดสอบ (หน่วยเวลา เป็น millisec)
50% หมายความว่า request จำนวน 50 % response กลับมาภายในเวลาที่ระบุ


Load Server 50% 90%
=====================================================
100 request/1 sec/1 ครั้ง Laptop 2410 3473
100 request/1 sec/3 ครั้ง Laptop 238 9781
100 request/1 sec/1 ครั้ง g5 348 1202
100 request/1 sec/3 ครั้ง g5 274 3543


เท่าที่ทดลองมายังไม่พบ Error 500
, Error 500 น่าจะเกิดจาก Fastcgi ที่ทำงานเกินค่าที่กำหนดไว้ใน
idle-timeout
ถ้า apache พบว่ามี process ที่ทำงานเกิน ก็จะ assume ว่า
process นั้น crash ไปแล้ว และจะ return Error 500 กลับไปให้ user
ส่วน process fcgi ก็อาจจะถูก kill และ start ใหม่


Laptop - 7 Processes - 100 request - 1 Sec - 1 loop


Laptop - 7 Processes - 100 request - 1 Sec - 3 loop


G5 - 7 Processes - 100 request - 1 Sec - 1 loop


G5 - 7 Processes - 100 request - 1 Sec - 3 loop

Related link from Roti

ลอง GWT

เพื่อให้เข้ากับยุคสมัย ผมก็เลย download GWT มาลองเล่นดู
เป้าหมายเพื่อให้รู้ว่า Architecture ของมันเป็นอย่างไร

เริ่มที่ขั้นแรกสุด ก็คือ ลองสร้าง project ง่ายๆพวก HelloWorld ขึ้นมา
(ในที่นี้ผมจะลองสร้างแต่ project ที่ใช้ eclipse เป็น IDE)

projectCreator -eclipse Hello

ผลลัพท์ของคำสั่งนี้ จะได้ file structure ออกมาดังนี้

+ src # empty directory
.classpath # eclipse build configuration file
.project # eclipse project file

จากนั้นก็ทำการ generate project files

applicationCreator -eclipse Hello pok.client.Hello

คำสั่ง applicationCreator จะสร้าง file ให้เราดังนี้

+ src
+ pok
+ client
Hello.java
+ public
Hello.html
Hello.gwt.xml
Hello.launch
Hello-shell
Hello-compile

  • Hello.launch
    เป็น configuration file ที่ Eclipse JDT ใช้
    ในการ run ใน Hosted Mode (mode ที่ใช้ java เป็นหลัก)
  • Hello-shell
    ใช้ run ใน Hosted Mode เหมือนกัน แต่เป็นการ
    start จาก command-line (แทนที่จะ run จากใน eclipse)
  • Hello-compile
    ใช้ในการ compile java file ให้เป็น javascript
    เพื่อที่จะได้นำไป deploy
  • Hello.java
    file หลัก ที่กำหนด Dynamic Behavior
  • Hello.html
    Template File
  • Hello.gwt.xml
    Configuration File


ลองดูหน้าตาของ Hello.java
public class Hello implements EntryPoint {

/**
* This is the entry point method.
*/

public void onModuleLoad() {
final Button button = new Button("Click me");
final Label label = new Label();

button.addClickListener(new ClickListener() {
public void onClick(Widget sender) {
if (label.getText().equals(""))
label.setText("Hello World!");
else
label.setText("");
}
});

RootPanel.get("slot1").add(button);
RootPanel.get("slot2").add(label);
}
}

จะเห็นว่าการเขียนมีลักษณะเดียวกับพวก Swing, AWT
ซึ่งในทาง Web Application เราจะเรียก Style การเขียนแบบนี้ว่า
"Component Centric" (Struts เป็นพวก Action Centric)

ลองดู Template บ้าง
<html>
<head>

<title>Wrapper HTML for Hello</title>
<style>
body,td,a,div,.p{font-family:arial,sans-serif}
div,td{color:#000000}
a:link,.w,.w a:link{color:#0000cc}
a:visited{color:#551a8b}
a:active{color:#ff0000}
</style>

<!-- -->
<!-- The module reference below is the link -->
<!-- between html and your Web Toolkit module -->
<!-- -->
<meta name='gwt:module' content='pok.Hello'>

</head>
<body>

<!-- -->
<!-- This script is required bootstrap stuff. -->
<!-- You can put it in the HEAD, but startup -->
<!-- is slightly faster if you include it here. -->
<!-- -->
<script language="javascript" src="gwt.js"></script>

<!-- OPTIONAL: include this if you want history support -->
<iframe id="__gwt_historyFrame" style="width:0;height:0;border:0"></iframe>

<h1>Hello</h1>

<table align=center>
<tr>
<td id="slot1"></td><td id="slot2"></td>
</tr>
</table>
</body>
</html>


จะเห็นว่าแนวทางจะเหมือน WebObject ของ Apple,
หรือถ้าพวก java ก็คือ Tapestry (Tapestry ได้แรงบันดาลใจจาก WebObject)
โดยมีการ map tag ใน Template เข้ากับ java component

ส่วนที่น่าสนใจ สำหรับ Hosted Mode ก็คือ
โปรแกรมส่วน Browser พัฒนาโดยใช้ Eclipse SWT
โดยมี Mozilla เป็น embed browser

เมื่อเราต้องการที่จะ run ทดสอบ
เราก็จะ start Hosted mode โดยใช้ script Hello-shell
ซึ่งการทำงานภายใน จะมีการเรียกใช้ tomcat เป็น server
และใช้ Servlet เป็นตัว provide Dynamic generate Javascript

ตอนพัฒนาใน mode ของ Java นี่ไม่ค่อยตื่นเต้นเท่าไร
ลองไปดู ผลลัพท์ที่ได้จากการ compile ดูบ้าง

structure ที่เกิดจากการ compile มีหน้าตาดังนี้
 + pok.Hello
6C111FAEE0653F4BBB3843A644BBC3B9.cache.html
6C111FAEE0653F4BBB3843A644BBC3B9.cache.xml
716702EAD597E14C305C093A453D09E0.cache.html
716702EAD597E14C305C093A453D09E0.cache.xml
BCF8E41DC1EA80A1D898F3F24A8B0E79.cache.html
BCF8E41DC1EA80A1D898F3F24A8B0E79.cache.xml
C23A120D5B9051FB13E2470D6906FE0B.cache.html
C23A120D5B9051FB13E2470D6906FE0B.cache.xml
gwt.js
hello.html
history.html
pok.Hello.nocache.html
tree_closed.gif
tree-open.gif
tree_white.gif

file ที่เราเรียกใช้งาน ก็คือ hello.html
ซึ่งภายในจะไป load gwt.js ขึ้นมาทำงาน
หน้าที่ของ gwt.js ก็คือมันจะ inject web framework เข้ามาทาง iframe
ดูได้จากคำสั่ง
function __gwt_injectWebModeFrame(name) {
if (document.body) {
var parts = __gwt_splitModuleNameRef(name);

// Insert an IFRAME
var iframe = document.createElement("iframe");
var selectorURL = parts[0] + parts[1] + ".nocache.html";
iframe.src = selectorURL;
iframe.style.border = '0px';
iframe.style.width = '0px';
iframe.style.height = '0px';
if (document.body.firstChild) {
document.body.insertBefore(iframe, document.body.firstChild);
} else {
document.body.appendChild(iframe);
}
} else {
// Try again in a moment.
//
window.setTimeout(function() { __gwt_injectWebModeFrame(name); }, __gwt_retryWaitMs);
}
}

ถ้าดูจาก source code จะเห็นว่า มัันไป load เอา pok.Hello.nocache.html ขึ้นมาทำงาน
ตามไปดูใน pok.Hello.nocache.html
ข้างในจะเป็น javascript ทีทำหน้าที่แค่เป็น Loader
โดยจะทำการตรวจสอบสภาพแวดล้อม ว่าเป็น browser แบบไหน
แล้วก็เรียกใช้ javascript ให้ถูกตัว
ซึ่งถ้าดูจาก source code จะเห็นว่า support browser อยู่ 4 พวก
ก็คือ Mozilla, IE, Safari, Opera
function selectScript() {
try {
var F;
var I = ["true", (F=window["prop$user.agent"],F())];

O(["true","oldmoz"],"6C111FAEE0653F4BBB3843A644BBC3B9");
O(["true","moz"],"6C111FAEE0653F4BBB3843A644BBC3B9");
O(["true","ie6"],"716702EAD597E14C305C093A453D09E0");
O(["true","safari"],"BCF8E41DC1EA80A1D898F3F24A8B0E79");
O(["true","opera"],"C23A120D5B9051FB13E2470D6906FE0B");

var strongName = O.answers[I[0]][I[1]];
location.replace(strongName + '.cache.html');
} catch (e) {
// intentionally silent on property failure
}
}

รหัส 6C111FAEE0653F4BBB3843A644BBC3B9 ก็คือ
ชื่อ file 6C111FAEE0653F4BBB3843A644BBC3B9.cache.html นั่นเอง

ภายใน file นี้ จะเป็น framework ที่ google เขียนขึ้นมาแล้ว
แต่เนื่องจากถูกลดรูปไว้แล้ว ก็เลยหมดปัญญาที่จะแกะ

ดูจากวิธีการของ framework นี้แล้ว ข้อดี ก็คือ
สำหรับคนที่ใช้ java อยู่แล้ว จะพัฒนาได้เร็ว
แถมยัง debug ได้ง่าย เนื่องจากยัง debug อยู่ใน java อยู่
เมื่อจะ deploy ก็ค่อยแปลงเป็น javascript

ข้อสงสัย สำหรับบางคน ก็คือแล้ว มันมีส่วน server ด้วยหรือเปล่า
ตัว GWT provide RPC API มาให้เราด้วย
วิธีใช้ ก็แค่เขียน Service โดย extend RemoteServiceServlet
import com.google.gwt.user.server.rpc.RemoteServiceServlet;

public class SchoolCalendarServiceImpl extends RemoteServiceServlet

โดยใน file gwt.xml ก็ระบุด้วยว่า
<module>
<inherits name='com.google.gwt.user.User'/>
<entry-point class='com.google.gwt.sample.dynatable.client.DynaTable'/>
<servlet path='/calendar' class='com.google.gwt.sample.dynatable.server.SchoolCalendarServiceImpl'/>
</module>

พอจะใช้ ก็แค่
// Initialize the service.
//
calService = (SchoolCalendarServiceAsync) GWT
.create(SchoolCalendarService.class);

// By default, we assume we'll make RPCs to a servlet, but see
// updateRowData(). There is special support for canned RPC responses.
// (Which is a totally demo hack, by the way :-)
//
ServiceDefTarget target = (ServiceDefTarget) calService;
target.setServiceEntryPoint("/calendar");


calService.getPeople(startRow, maxRows, new AsyncCallback() {
public void onFailure(Throwable caught) {
acceptor.failed(caught);
}

public void onSuccess(Object result) {
Person[] people = (Person[]) result;
lastStartRow = startRow;
lastMaxRows = maxRows;
lastPeople = people;
pushResults(acceptor, startRow, people);
}

});


ดูแล้ว ก็ไม่ยากอะไร
สิ่งที่ GWT ไม่ได้เตรียมมาให้ สำหรับส่วน server ก็คือ
script ที่ใช้ในการ pack war เพื่อไป deploy remote service
บน Application Server ที่เราต้องการ

Related link from Roti

Wednesday, May 17, 2006

Tapestry-Spring

ใน Tapestry4 เราสามารถ inject spring bean เข้าไปใน Tapestry Page
โดยใช้ plugin ที่ชื่อ Tapestry-Spring

ตัวอย่างวิธีการใช้
public abstract class Home extends BasePage {

@InjectPage("test/Result")
abstract public Result getResultPage();

@Persist("client")
@InitialValue("literal:MSFT")
public abstract String getStockId();

@InjectSpring(value="stockService")
public abstract StockService getStockService();

public IPage onOK() {
Result rs = getResultPage();
rs.setStockValue(getStockService().getStockValue(getStockId()));
return rs;
}
}


Note: Architecture Tapestry มี conflict กับการใช้ spring bean แบบที่ไม่ใช่ Singleton
ดังนั้นวิธีนี้ใช้ไม่ได้กับ case ที่ไม่ใช่ Singleton

ที่น่าสนใจ ไม่ได้อยู่ตรงวิธีใช้ แต่อยู่ตรงวิธีที่เขา implement
ลองดู source code ในส่วนของการ generate code
ซึ่งเขาใช้วิธี generate code ลงไปแทนที่ abstract method ข้างบน
        BodyBuilder builder = new BodyBuilder();

builder.begin();
builder.addln("try");
builder.begin();
builder.addln(
"return ({0}) {1}.getBean(\"{2}\");",
propertyType.getName(),
beanFactoryName,
beanName);
builder.end();
builder.addln("catch (Exception ex)");
builder.begin();
builder.addln("throw new {0}(ex.getMessage(), {1}, ex);", ApplicationRuntimeException.class
.getName(), locationName);
builder.end();
builder.end();

String methodName = op.getAccessorMethodName(propertyName);

MethodSignature sig = new MethodSignature(propertyType, methodName, null, null);

op.addMethod(Modifier.PUBLIC, sig, builder.toString(), location);

Related link from Roti

Monday, May 15, 2006

Rails Memo #1

ผลพวงจาก Blognone SIG #1 ก็เลยกลับมานั่งเขียน Memo
ผู้เรียนทั้งหลายจะได้มีไว้ทบทวนกันลืม
(เพื่อไม่ให้งง ผมจะอธิบายถึง default assumption ก่อน
กรณี exception หรือ customize จะยังไม่พูดถึง)

เริ่มด้วยเรื่องแรกสุดก่อน ก็คือ Controller และ View

ประเด็นแรก
เริ่มที่เรื่องชื่อของ controller ก่อน
ยกตัวอย่าง
class ProductsController
จะถูกเก็บไว้ใน file products_controller.rb

class HomeController
จะถูกเก็บไว้ใน file home_controller.rb

กรณีที่ชื่อประกอบจากหลายคำ เช่น
class AdminAreaController
ชื่อ file ก็ต้องเป็น admin_area_controller.rb

class PropertiesAdminController
ชื่อ file ก็ต้องเป็น properties_admin_controller.rb

Note: ที่ระดับ controller นี้ จะไม่มีประเด็นเรื่องพหูพจน์หรือเอกพจน์มาเกี่ยวข้อง
(อยากตั้งชื่ออย่างไรก็ว่าไป)

เมื่อเข้าใจวิธีตั้งชื่อแล้ว ก็มาถึงวิธี map url เข้ากับ controller
หลักการก็คือ path แรกสุดของ url จะถูก map เข้ากับ controller
เช่น
http://host:port/products
จะเรียกใช้ controller ProductController

http://host:port/home
อันนี้จะเรียกใช้ class HomeController

http://host:port/admin_area
=> class AdminAreaController

http://host:port/products_admin
=> class ProductsAdminController

url ที่ browser ส่งเข้ามานั้น
Rails คาดหวังว่าจะมีการระบุ action หรือ method มาด้วย เช่น
http://host:port/products/list
ในกรณีนี้ "list" ก็คือชื่อ action(method) ที่ต้องการเรียกใช้
หลักการที่ rails ใช้ solve สำหรับชื่อ action ก็คือ
  • ถ้า controller นั้นมี method ที่ชื่อว่า list อยู่
    ,method นั้นก็จะถูกเรียกใช้
  • กรณีที่ controller นั้นไม่มี method ที่ชื่อ list
    ,rails ก็จะ bypass controller และส่งต่อให้ view ที่ชื่อ list.rhtml ทำงานแทน


แต่ถ้าเราไม่ระบุ action มาเลย เช่น
http://host:port/products
,Rails ก็จะเรียกใช้ default action ที่ชื่อ index


ประเด็นที่สองก็คือ เรื่อง
ความสัมพันธ์ระหว่าง ชื่อของ Controller กับ
Directory ที่เก็บ View
สมมติว่า url ที่เรียกเข้ามามีหน้าตาแบบนี้
http://host:port/products/list
Rails จะมองหา view ที่ชื่อ list.rhtml
ที่เก็บอยู่ภายใต้ Directory $PROJECT/app/views/products/

จะเห็นว่าชื่อ Directory ที่เก็บ View จะเป็นชื่อเดียวกับ Controller

Related link from Roti