/*
* Copyright (c) 2011 Apple Inc. All rights reserved.
*
* @APPLE_APACHE_LICENSE_HEADER_START@
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @APPLE_APACHE_LICENSE_HEADER_END@
*/
//
// BlockLifetime.m
// Copyright (c) 2008-2011 Apple Inc. All rights reserved.
//
#import "Configuration.h"
#import "WhiteBoxTest.h"
/*
BlockLifetime implements the following test scenario:
Allocate a test block.
Perform full collection.
Verify test block was scanned exactly once.
Verify test block remained local.
Assign test block to a global variable.
Verify test block became global.
Clear global variable, assign test block to ivar.
Loop:
run generational collection
verify block is scanned exactly once and age decrements by 1
until test block age becomes 0.
Run a full collection (to clear write barriers).
Verify block is scanned exactly once.
Run a generational collection.
Verify block *not* scanned.
Clear test block ivar.
Run generational collection.
Verify block *not* scanned and *not* collected.
Run a full collection.
Verify block was collected.
*/
@interface BlockLifetime : WhiteBoxTest {
BOOL _becameGlobal;
BOOL _scanned;
BOOL _collected;
BOOL _shouldCollect;
uint32_t _age;
id _testBlock;
vm_address_t _disguisedTestBlock;
}
@end
@interface BlockLifetimeTestObject : NSObject
{
@public
id anObject;
}
@end
@implementation BlockLifetimeTestObject
@end
@interface BlockLifetime(internal)
- (void)globalTransition;
- (void)ageLoop;
- (void)clearBarriers;
- (void)generationalScan;
- (void)verifyGenerationalScan;
- (void)uncollectedCheck;
- (void)collectedCheck;
@end
@implementation BlockLifetime
static id _globalTestBlock = nil;
- (void)allocLocalBlock
{
_disguisedTestBlock = [self disguise:[BlockLifetimeTestObject new]];
[self testThreadStackBuffer][0] = (id)[self undisguise:_disguisedTestBlock];
}
- (void)makeTestBlockGlobal
{
_globalTestBlock = (id)[self undisguise:_disguisedTestBlock];
[self testThreadStackBuffer][0] = nil;
}
// First step: allocate a new object and request a full collection.
// The new object should be thread local at this point.
- (void)performTest
{
[self performSelectorOnTestThread:@selector(allocLocalBlock)];
// Run a full collection
[self requestFullCollectionWithCompletionCallback:^{ [self globalTransition]; }];
}
// defeat the write barrier card optimization by touching the object
- (void)touchObject
{
BlockLifetimeTestObject *object = (BlockLifetimeTestObject *)[self undisguise:_disguisedTestBlock];
object->anObject = self;
object->anObject = nil;
}
// verify the block got scanned, flag an error if it did not
#define verifyScanned() \
do { if (!_scanned) { [self fail:[NSString stringWithFormat:@"block was not scanned as expected in
// Second step: transition the block to global
- (void)globalTransition
{
verifyScanned();
// verify the block isn't global already
if (_becameGlobal) {
[self fail:@"block became global prematurely"];
[self testFinished];
return;
}
// make the block go global and verify that we saw the transition
// it's possible we never saw the transition because it went global before we started watching
// that situation probably merits investigation
[self performSelectorOnTestThread:@selector(makeTestBlockGlobal)];
if (!_becameGlobal) {
[self fail:@"block failed to become global"];
[self testFinished];
return;
}
// now the only reference to the test block is in _globalTestBlock
// do generational collections until the block becomes old
[self touchObject];
[self requestGenerationalCollectionWithCompletionCallback:^{ [self ageLoop]; }];
}
// This method runs generational collections until the block reaches age 0
- (void)ageLoop
{
SEL next;
verifyScanned();
if (_age != 0) {
// set up for the next iteration
[self touchObject];
[self requestGenerationalCollectionWithCompletionCallback:^{ [self ageLoop]; }];
} else {
// We are done iterating, the block is now age 0.
// Now we want to verify that the block does *NOT* get scanned during a generational collection.
// First must run a full collection to clear write barrier cards.
[self requestFullCollectionWithCompletionCallback:^{ [self clearBarriers]; }];
}
}
// The full collection does not clear write barrier cards. Run a generational to clear them.
- (void)clearBarriers
{
verifyScanned();
[self requestGenerationalCollectionWithCompletionCallback:^{ [self generationalScan]; }];
}
// Now the object is old and should have the write barrier cards cleared.
// Verify that a generational scan does *not* scan the block.
- (void)generationalScan
{
verifyScanned();
[self requestGenerationalCollectionWithCompletionCallback:^{ [self verifyGenerationalScan]; }];
}
// The last scan was generational, the block is old, and write barriers were cleared. Verify the block was *not* scanned.
- (void)verifyGenerationalScan
{
if (_scanned) {
[self fail:@"age 0 block scanned during generational collection"];
[self testFinished];
return;
}
// Now just verify that the block gets collected
_globalTestBlock = nil;
_shouldCollect = YES;
// Run a generational and verify the old block was not collected.
[self requestGenerationalCollectionWithCompletionCallback:^{ [self uncollectedCheck]; }];
}
// We expect that the old garbage block would not be collected by a generational collection
- (void)uncollectedCheck
{
if (_collected) {
[self fail:@"old block collected by generational collection"];
[self testFinished];
return;
}
// now run a full collection
[self requestFullCollectionWithCompletionCallback:^{ [self collectedCheck]; }];
}
// A full collection should have collected the garbage block.
- (void)collectedCheck
{
if (!_collected)
[self fail:@"block not collected after full collection"];
else
[self passed];
[self testFinished];
}
// Watch for our test block becoming global.
- (void)blockBecameGlobal:(void *)block withAge:(uint32_t)age
{
if (block == [self undisguise:_disguisedTestBlock]) {
_becameGlobal = YES;
_age = age;
}
[super blockBecameGlobal:block withAge:age];
}
// Monitor the aging of our test block
- (void)blockMatured:(void *)block newAge:(uint32_t)age
{
if (block == [self undisguise:_disguisedTestBlock]) {
if (age != _age - 1)
[self fail:@"Age decrease != 1 after mature"];
_age = age;
}
[super blockMatured:block newAge:age];
}
// Check if the test block is in the garbage list
- (void)endHeapScanWithGarbage:(void **)garbage_list count:(size_t)count
{
if ([self block:[self undisguise:_disguisedTestBlock] isInList:garbage_list count:count]) {
if (!_shouldCollect)
[self fail:@"block was collected prematurely"];
_collected = YES;
}
[super endHeapScanWithGarbage:garbage_list count:count];
}
// Monitor when our test block is scanned, and verify it never gets scanned twice in the same collection.
- (void)scanBlock:(void *)block endAddress:(void *)end
{
if (block == [self undisguise:_disguisedTestBlock]) {
if (_scanned) {
[self fail:@"block scanned twice"];
}
_scanned = YES;
}
[super scanBlock:block endAddress:end];
}
- (void)scanBlock:(void *)block endAddress:(void *)end withLayout:(const unsigned char *)map
{
[self scanBlock:block endAddress:end];
[super scanBlock:block endAddress:end withLayout:map];
}
@end