اختبار الأداء
يضمن اختبار الأداء قدرة تطبيقك على التعامل مع أحمال المستخدمين المتوقعة والاستجابة بسرعة في ظروف مختلفة. في هذا الدرس، سنستكشف اختبار الحمل والمقارنة المرجعية والتنميط وأدوات مثل k6 وArtillery لقياس أداء التطبيق وتحسينه.
لماذا يهم اختبار الأداء
يساعدك اختبار الأداء على:
- تحديد الاختناقات قبل أن تؤثر على المستخدمين
- ضمان أوقات استجابة مقبولة تحت الحمل
- تحديد سعة النظام وحدود قابلية التوسع
- التحقق من صحة قرارات حجم البنية التحتية
- منع تراجع الأداء مع كل إصدار
- تحسين استعلامات قاعدة البيانات واستدعاءات API
- قياس وتحسين استخدام الموارد
مفهوم أساسي: اختبار الأداء ليس فقط عن السرعة—بل يتعلق بضمان السلوك المتسق والقابل للتنبؤ في ظروف واقعية والتدهور الرشيق تحت الضغط.
أنواع اختبار الأداء
تخدم أنواع الاختبار المختلفة أغراضًا مختلفة:
- اختبار الحمل: اختبار سلوك النظام تحت ظروف الحمل المتوقعة
- اختبار الضغط: الاختبار خارج السعة العادية لإيجاد نقاط الانهيار
- اختبار الذروة: اختبار الزيادات المفاجئة والدراماتيكية في الحمل
- اختبار النقع: اختبار الحمل المستمر على مدى فترات ممتدة للكشف عن تسريبات الذاكرة
- اختبار قابلية التوسع: اختبار كيفية توسع النظام مع زيادة الحمل
- اختبار الحجم: الاختبار بكميات كبيرة من البيانات
المقارنة المرجعية الأساسية في PHPUnit
ابدأ بقياسات توقيت بسيطة في اختباراتك:
<?php
namespace Tests\Performance;
use App\Models\Product;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class ProductSearchPerformanceTest extends TestCase
{
use RefreshDatabase;
public function test_search_completes_within_time_limit()
{
// بذر بيانات الاختبار
Product::factory()->count(1000)->create();
$startTime = microtime(true);
// تنفيذ البحث
$results = Product::where('name', 'like', '%test%')
->with(['category', 'images'])
->paginate(20);
$duration = microtime(true) - $startTime;
// التأكد من أن وقت الاستجابة مقبول (< 100 مللي ثانية)
$this->assertLessThan(0.1, $duration,
"استغرق البحث {$duration}ث، المتوقع < 0.1ث"
);
// التأكد من صحة النتائج
$this->assertGreaterThan(0, $results->total());
}
public function test_bulk_insert_performance()
{
$data = Product::factory()->count(1000)->make()->toArray();
$startTime = microtime(true);
Product::insert($data);
$duration = microtime(true) - $startTime;
// يجب أن يكون الإدراج المجمع سريعًا (< 1ث لـ 1000 سجل)
$this->assertLessThan(1.0, $duration,
"استغرق الإدراج المجمع لـ 1000 سجل {$duration}ث"
);
$this->assertEquals(1000, Product::count());
}
}
أداء استعلامات قاعدة البيانات
يساعد تسجيل الاستعلامات في Laravel في تحديد الاستعلامات البطيئة:
<?php
namespace Tests\Performance;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class QueryPerformanceTest extends TestCase
{
public function test_no_n_plus_one_queries()
{
// تمكين تسجيل الاستعلامات
DB::enableQueryLog();
$posts = Post::with(['author', 'comments'])->limit(10)->get();
// تحميل جميع التعليقات
foreach ($posts as $post) {
$commentCount = $post->comments->count();
}
$queries = DB::getQueryLog();
// يجب تنفيذ استعلام واحد فقط (للمنشورات مع التحميل المسبق)
// وليس N+1 استعلام (1 للمنشورات + N للتعليقات)
$this->assertLessThanOrEqual(1, count($queries),
"تم اكتشاف استعلام N+1. تم تنفيذ " . count($queries) . " استعلامات"
);
}
public function test_query_execution_time()
{
DB::enableQueryLog();
$startTime = microtime(true);
$results = Product::select('products.*')
->join('categories', 'categories.id', '=', 'products.category_id')
->where('categories.active', true)
->whereNull('products.deleted_at')
->orderBy('products.created_at', 'desc')
->take(100)
->get();
$duration = microtime(true) - $startTime;
$queries = DB::getQueryLog();
// تسجيل الاستعلامات البطيئة للتحليل
foreach ($queries as $query) {
if ($query['time'] > 50) { // 50 مللي ثانية
$this->fail("تم اكتشاف استعلام بطيء: {$query['query']}" .
" استغرق {$query['time']}مللي ثانية");
}
}
$this->assertLessThan(0.1, $duration);
}
}
اختبار أداء نقاط نهاية API
اختبر أوقات استجابة نقاط نهاية HTTP:
<?php
namespace Tests\Performance;
use Tests\TestCase;
class ApiPerformanceTest extends TestCase
{
public function test_api_response_time()
{
$iterations = 10;
$times = [];
for ($i = 0; $i < $iterations; $i++) {
$startTime = microtime(true);
$response = $this->getJson('/api/products?page=1&per_page=20');
$duration = microtime(true) - $startTime;
$times[] = $duration;
$response->assertOk();
}
$averageTime = array_sum($times) / count($times);
$maxTime = max($times);
// التأكد من متوسط وقت الاستجابة
$this->assertLessThan(0.2, $averageTime,
"متوسط وقت الاستجابة: {$averageTime}ث"
);
// التأكد من أقصى وقت استجابة
$this->assertLessThan(0.5, $maxTime,
"أقصى وقت استجابة: {$maxTime}ث"
);
}
public function test_concurrent_requests_performance()
{
// محاكاة الطلبات المتزامنة
$responses = [];
$startTime = microtime(true);
// في الاختبار المتزامن الحقيقي، استخدم أدوات مثل k6 أو Artillery
// هذا مثال مبسط
for ($i = 0; $i < 50; $i++) {
$responses[] = $this->getJson('/api/products');
}
$totalTime = microtime(true) - $startTime;
foreach ($responses as $response) {
$response->assertOk();
}
// يجب أن تكتمل 50 طلبًا بسرعة معقولة
$this->assertLessThan(10.0, $totalTime,
"استغرقت 50 طلبًا متتاليًا {$totalTime}ث"
);
}
}
أفضل ممارسة: قم بتشغيل اختبارات الأداء في بيئة تطابق الإنتاج بشكل وثيق—نفس حجم قاعدة البيانات، أجهزة مماثلة، ظروف الشبكة، وتكوينات التخزين المؤقت.
اختبار الحمل باستخدام k6
k6 هي أداة اختبار حمل حديثة مبنية للمطورين. قم بتثبيتها وإنشاء نصوص اختبار:
// التثبيت: brew install k6 (macOS) أو snap install k6 (Linux)
// load-test.js
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Rate } from 'k6/metrics';
// المقاييس المخصصة
const errorRate = new Rate('errors');
// تكوين الاختبار
export const options = {
stages: [
{ duration: '30s', target: 10 }, // الارتفاع إلى 10 مستخدمين
{ duration: '1m', target: 50 }, // الارتفاع إلى 50 مستخدمًا
{ duration: '2m', target: 50 }, // البقاء عند 50 مستخدمًا
{ duration: '30s', target: 0 }, // الانخفاض إلى 0 مستخدم
],
thresholds: {
http_req_duration: ['p(95)<500'], // 95% من الطلبات < 500 مللي ثانية
http_req_failed: ['rate<0.01'], // معدل الخطأ < 1%
errors: ['rate<0.1'], // معدل الخطأ المخصص < 10%
},
};
export default function () {
// اختبار الصفحة الرئيسية
let homeRes = http.get('https://example.com');
check(homeRes, {
'حالة الصفحة الرئيسية 200': (r) => r.status === 200,
'تحميل الصفحة الرئيسية في < 200 مللي ثانية': (r) => r.timings.duration < 200,
});
sleep(1);
// اختبار نقطة نهاية API
let apiRes = http.get('https://example.com/api/products', {
headers: {
'Accept': 'application/json',
},
});
const apiCheck = check(apiRes, {
'حالة API 200': (r) => r.status === 200,
'API تُرجع JSON': (r) => r.headers['Content-Type'].includes('json'),
'وقت استجابة API جيد': (r) => r.timings.duration < 300,
});
errorRate.add(!apiCheck);
sleep(2);
// اختبار البحث
let searchRes = http.get('https://example.com/api/products?search=laptop');
check(searchRes, {
'البحث يُرجع نتائج': (r) => r.json('data').length > 0,
});
sleep(1);
}
// التشغيل: k6 run load-test.js
سيناريوهات k6 المتقدمة
أنشئ سيناريوهات اختبار أكثر تطورًا:
// advanced-load-test.js
import http from 'k6/http';
import { check, group } from 'k6';
export const options = {
scenarios: {
// السيناريو 1: حمل ثابت
constant_load: {
executor: 'constant-vus',
vus: 20,
duration: '2m',
},
// السيناريو 2: اختبار الذروة
spike_test: {
executor: 'ramping-vus',
startTime: '2m',
stages: [
{ duration: '10s', target: 100 },
{ duration: '1m', target: 100 },
{ duration: '10s', target: 0 },
],
},
// السيناريو 3: اختبار الضغط
stress_test: {
executor: 'ramping-arrival-rate',
startTime: '4m',
startRate: 50,
timeUnit: '1s',
stages: [
{ duration: '2m', target: 200 },
{ duration: '5m', target: 200 },
{ duration: '2m', target: 0 },
],
},
},
thresholds: {
'http_req_duration{scenario:constant_load}': ['p(95)<300'],
'http_req_duration{scenario:spike_test}': ['p(95)<1000'],
'http_req_failed': ['rate<0.05'],
},
};
export default function () {
group('تصفح المنتجات', function () {
let res = http.get('https://example.com/api/products');
check(res, {
'تم تحميل المنتجات': (r) => r.status === 200,
});
});
group('عرض تفاصيل المنتج', function () {
let res = http.get('https://example.com/api/products/123');
check(res, {
'تم تحميل تفاصيل المنتج': (r) => r.status === 200,
'المنتج لديه الحقول المطلوبة': (r) => {
const body = r.json();
return body.name && body.price && body.description;
},
});
});
}
اختبار الحمل باستخدام Artillery
Artillery هي أداة اختبار حمل قوية أخرى مع تكوين YAML:
# التثبيت: npm install -g artillery
# artillery-test.yml
config:
target: 'https://example.com'
phases:
- duration: 60
arrivalRate: 10
name: 'الإحماء'
- duration: 120
arrivalRate: 50
name: 'حمل مستمر'
- duration: 60
arrivalRate: 100
name: 'ذروة'
http:
timeout: 10
plugins:
expect: {}
ensure:
maxErrorRate: 1
p95: 500
p99: 1000
scenarios:
- name: 'التصفح والبحث'
flow:
- get:
url: '/'
expect:
- statusCode: 200
- contentType: text/html
- think: 2
- get:
url: '/api/products'
expect:
- statusCode: 200
- hasProperty: data
- think: 3
- post:
url: '/api/search'
json:
query: 'laptop'
filters:
category: 'electronics'
expect:
- statusCode: 200
- hasProperty: results
- think: 5
- name: 'تدفق مصادقة المستخدم'
weight: 30
flow:
- post:
url: '/api/login'
json:
email: 'test@example.com'
password: 'password123'
capture:
- json: '$.token'
as: 'authToken'
- get:
url: '/api/user/profile'
headers:
Authorization: 'Bearer {{ authToken }}'
expect:
- statusCode: 200
# التشغيل: artillery run artillery-test.yml
# التشغيل مع تقرير: artillery run --output report.json artillery-test.yml
# إنشاء تقرير HTML: artillery report report.json
مهم: اختبر دائمًا ضد بيئات غير الإنتاج أو احصل على إذن صريح قبل اختبار الحمل على أنظمة الإنتاج. يمكن أن تتسبب حركة المرور غير المتوقعة في انقطاعات.
تنميط تطبيقات PHP
استخدم Xdebug أو XHProf للتنميط التفصيلي:
// تثبيت Xdebug: pecl install xdebug
// تمكين التنميط في php.ini:
xdebug.mode=profile
xdebug.output_dir=/tmp
xdebug.profiler_enable_trigger=1
// تشغيل التنميط بمعامل URL:
// https://example.com/slow-page?XDEBUG_PROFILE=1
// التحليل بأدوات مثل:
// - KCacheGrind (Linux)
// - QCacheGrind (macOS/Windows)
// - Webgrind (قائم على الويب)
Laravel Telescope لمراقبة الأداء
استخدم Telescope لمراقبة أداء التطبيق في التطوير:
// تثبيت Telescope
composer require laravel/telescope --dev
php artisan telescope:install
php artisan migrate
// التكوين في config/telescope.php
'watchers' => [
Watchers\QueryWatcher::class => [
'enabled' => env('TELESCOPE_QUERY_WATCHER', true),
'slow' => 50, // تسجيل الاستعلامات أبطأ من 50 مللي ثانية
],
Watchers\RequestWatcher::class => [
'enabled' => env('TELESCOPE_REQUEST_WATCHER', true),
'size_limit' => 64,
],
],
// الوصول في: http://localhost/telescope
// عرض الاستعلامات البطيئة، مدة الطلبات، استخدام الذاكرة، إلخ.
تحسين استعلامات قاعدة البيانات
قم بتحسين استعلامات قاعدة البيانات البطيئة المحددة في الاختبارات:
// قبل: مشكلة استعلام N+1
$users = User::all();
foreach ($users as $user) {
echo $user->posts->count(); // ينفذ استعلامًا لكل مستخدم
}
// بعد: التحميل المسبق
$users = User::withCount('posts')->get();
foreach ($users as $user) {
echo $user->posts_count; // لا استعلامات إضافية
}
// قبل: تحميل بيانات غير ضرورية
$products = Product::all(); // يحمل جميع الأعمدة، جميع الصفوف
// بعد: تحديد الأعمدة المطلوبة فقط وتحديد الصفوف
$products = Product::select(['id', 'name', 'price'])
->where('active', true)
->limit(100)
->get();
// استخدام الفهرسة للأعمدة المستعلم عنها بشكل متكرر
Schema::table('products', function (Blueprint $table) {
$table->index('category_id');
$table->index(['active', 'created_at']);
$table->fullText('name'); // لاستعلامات البحث
});
التخزين المؤقت للأداء
نفذ التخزين المؤقت لتقليل حمل قاعدة البيانات:
<?php
// تخزين الاستعلامات المكلفة مؤقتًا
$products = Cache::remember('products.featured', 3600, function () {
return Product::with(['images', 'category'])
->where('featured', true)
->orderBy('popularity', 'desc')
->take(10)
->get();
});
// تخزين استجابات API مؤقتًا
Route::get('/api/products', function () {
return Cache::remember('api.products.' . request('page', 1), 600, function () {
return Product::paginate(20);
});
});
// اختبار أداء التخزين المؤقت
public function test_cache_improves_performance()
{
// الاستدعاء الأول (فشل التخزين المؤقت)
$start = microtime(true);
$result1 = $this->getJson('/api/products');
$time1 = microtime(true) - $start;
// الاستدعاء الثاني (نجاح التخزين المؤقت)
$start = microtime(true);
$result2 = $this->getJson('/api/products');
$time2 = microtime(true) - $start;
// يجب أن تكون الاستجابة المخزنة مؤقتًا أسرع بكثير
$this->assertLessThan($time1 / 2, $time2,
"يجب أن يحسن التخزين المؤقت الأداء بنسبة 50% على الأقل"
);
}
تحذير: التحسين المبكر هو جذر كل شر. قم دائمًا بالتنميط والقياس قبل التحسين. ركز على الاختناقات الفعلية، وليس النظرية.
تنميط الذاكرة
راقب استخدام الذاكرة في الاختبارات:
<?php
public function test_bulk_operation_memory_usage()
{
$memoryBefore = memory_get_usage(true);
// معالجة مجموعة بيانات كبيرة
Product::chunk(1000, function ($products) {
foreach ($products as $product) {
// معالجة المنتج
$product->update(['processed' => true]);
}
});
$memoryAfter = memory_get_usage(true);
$memoryUsed = $memoryAfter - $memoryBefore;
// التأكد من أن استخدام الذاكرة معقول (< 50 ميجابايت)
$this->assertLessThan(50 * 1024 * 1024, $memoryUsed,
"استخدام الذاكرة: " . round($memoryUsed / 1024 / 1024, 2) . "ميجابايت"
);
}
public function test_memory_leak_detection()
{
$iterations = 100;
$memoryReadings = [];
for ($i = 0; $i < $iterations; $i++) {
// تنفيذ العملية
$users = User::with('posts')->get();
// تسجيل الذاكرة
$memoryReadings[] = memory_get_usage(true);
unset($users);
}
// التحقق مما إذا كانت الذاكرة تزداد باستمرار (تسريب محتمل)
$firstHalf = array_slice($memoryReadings, 0, 50);
$secondHalf = array_slice($memoryReadings, 50);
$avgFirst = array_sum($firstHalf) / count($firstHalf);
$avgSecond = array_sum($secondHalf) / count($secondHalf);
// يجب ألا تزداد الذاكرة بأكثر من 20%
$this->assertLessThan($avgFirst * 1.2, $avgSecond,
"تم اكتشاف تسريب محتمل للذاكرة"
);
}
تمرين 1: أنشئ نص اختبار حمل k6 لتدفق الدفع في التجارة الإلكترونية يحاكي: (1) تصفح المنتجات، (2) الإضافة إلى السلة، (3) عرض السلة، (4) بدء الدفع، و(5) إكمال الطلب. قم بتكوين عتبات لأوقات الاستجابة في النسبة المئوية 95.
تمرين 2: اكتب اختبارات أداء لميزة تصدير البيانات التي تولد ملفات CSV بـ 100,000 صف. اختبر استخدام الذاكرة ووقت التنفيذ وتأكد من عدم انتهاء المهلة. قم بالتحسين باستخدام التجزئة إذا لزم الأمر.
تمرين 3: قم بتنميط صفحة لوحة معلومات بطيئة باستخدام Telescope أو Xdebug. حدد أكبر 3 اختناقات في الأداء (استعلامات بطيئة، مشاكل N+1، فهارس مفقودة) ونفذ الإصلاحات. تحقق من التحسينات باستخدام مقارنات قبل/بعد.
الملخص
في هذا الدرس، غطينا استراتيجيات اختبار أداء شاملة:
- فهم أنواع مختلفة من اختبار الأداء (الحمل، الضغط، الذروة، النقع)
- المقارنة المرجعية الأساسية مع PHPUnit وقياسات التوقيت
- تحديد مشاكل استعلامات N+1 وتحسين استعلامات قاعدة البيانات
- اختبار الحمل باستخدام k6 وArtillery لسيناريوهات المستخدم الواقعية
- تنميط تطبيقات PHP باستخدام Xdebug وTelescope
- تنفيذ استراتيجيات التخزين المؤقت لتحسين الأداء
- تنميط الذاكرة واكتشاف التسريبات
- تعيين عتبات الأداء واتفاقيات مستوى الخدمة
اختبار الأداء هو عملية مستمرة. قم بدمج هذه الاختبارات في خط أنابيب CI/CD الخاص بك للكشف عن التراجع مبكرًا وضمان بقاء تطبيقك سريعًا وسريع الاستجابة مع نموه.